debug
This commit is contained in:
119
include/condorcet/src/Algo/Method.php
Normal file
119
include/condorcet/src/Algo/Method.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?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\Algo;
|
||||
|
||||
use CondorcetPHP\Condorcet\{CondorcetVersion, Election, Result};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{InternalModulesAPI, Throws};
|
||||
use CondorcetPHP\Condorcet\Throwable\CandidatesMaxNumberReachedException;
|
||||
|
||||
// Generic for Algorithms
|
||||
abstract class Method
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
private const METHOD_NAME = ['abstractMethod'];
|
||||
|
||||
public const IS_PROPORTIONAL = false;
|
||||
|
||||
public static ?int $MaxCandidates = null;
|
||||
|
||||
protected readonly \WeakReference $selfElection;
|
||||
protected ?Result $Result = null;
|
||||
|
||||
// Internal precision
|
||||
public const DECIMAL_PRECISION = 9;
|
||||
|
||||
// Static
|
||||
|
||||
final public static function setOption(string $optionName, array|\BackedEnum|int|float|string $optionValue): bool
|
||||
{
|
||||
$optionVar = 'option'.ucfirst($optionName);
|
||||
|
||||
static::${$optionVar} = $optionValue;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
#[Throws(CandidatesMaxNumberReachedException::class)]
|
||||
public function __construct(Election $mother)
|
||||
{
|
||||
$this->setElection($mother);
|
||||
|
||||
if (static::$MaxCandidates !== null && $mother->countCandidates() > static::$MaxCandidates) {
|
||||
throw new CandidatesMaxNumberReachedException(static::METHOD_NAME[0], static::$MaxCandidates);
|
||||
}
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$r = get_object_vars($this);
|
||||
unset($r['selfElection']);
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
public function setElection(Election $election): void
|
||||
{
|
||||
$this->selfElection = \WeakReference::create($election);
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getElection(): Election
|
||||
{
|
||||
return $this->selfElection->get(); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getResult(): Result
|
||||
{
|
||||
// Cache
|
||||
if ($this->Result !== null) {
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
$this->compute();
|
||||
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
}
|
||||
|
||||
abstract protected function getStats(): array;
|
||||
|
||||
protected function createResult(array $result): Result
|
||||
{
|
||||
$optionsList = array_keys((new \ReflectionClass(static::class))->getStaticProperties());
|
||||
$optionsList = array_filter($optionsList, static function (string $name): bool {
|
||||
return str_starts_with($name, 'option');
|
||||
});
|
||||
|
||||
$methodOptions = [];
|
||||
|
||||
foreach ($optionsList as $oneOption) {
|
||||
$methodOptions[mb_substr($oneOption, 6)] = static::${$oneOption};
|
||||
}
|
||||
|
||||
return new Result(
|
||||
fromMethod: static::METHOD_NAME[0],
|
||||
byClass: $this::class,
|
||||
election: $this->getElection(),
|
||||
result: $result,
|
||||
stats: ($this->getElection()->getStatsVerbosity()->value > StatsVerbosity::NONE->value) ? $this->getStats() : null,
|
||||
seats: (static::IS_PROPORTIONAL) ? $this->getElection()->getNumberOfSeats() : null,
|
||||
methodOptions: $methodOptions
|
||||
);
|
||||
}
|
||||
}
|
25
include/condorcet/src/Algo/MethodInterface.php
Normal file
25
include/condorcet/src/Algo/MethodInterface.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?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\Algo;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\InternalModulesAPI;
|
||||
use CondorcetPHP\Condorcet\{Election, Result};
|
||||
|
||||
// Interface with the aim of verifying the good modular implementation of algorithms.
|
||||
#[InternalModulesAPI]
|
||||
interface MethodInterface
|
||||
{
|
||||
public function __construct(Election $election);
|
||||
|
||||
public function setElection(Election $election): void;
|
||||
public function getResult(): Result;
|
||||
}
|
94
include/condorcet/src/Algo/Methods/Borda/BordaCount.php
Normal file
94
include/condorcet/src/Algo/Methods/Borda/BordaCount.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/*
|
||||
Part of BORDA COUNT method Module - From the original Condorcet 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\Algo\Methods\Borda;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class BordaCount extends Method implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['BordaCount', 'Borda Count', 'Borda', 'Méthode Borda'];
|
||||
|
||||
public static int $optionStarting = 1;
|
||||
|
||||
protected ?array $Stats = null;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$stats = [];
|
||||
|
||||
foreach ($this->Stats as $candidateKey => $oneScore) {
|
||||
$stats[(string) $election->getCandidateObjectFromKey($candidateKey)] = $oneScore;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: BORDA ALGORITHM. :://
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$score = [];
|
||||
|
||||
foreach (array_keys($election->getCandidatesList()) as $oneCandidateKey) {
|
||||
$score[$oneCandidateKey] = 0;
|
||||
}
|
||||
|
||||
foreach ($election->getVotesManager()->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
$CandidatesRanked = 0;
|
||||
$oneRanking = $oneVote->getContextualRankingWithoutSort($election);
|
||||
|
||||
foreach ($oneRanking as $oneRank) {
|
||||
$rankScore = 0.0;
|
||||
foreach ($oneRank as $oneCandidateInRank) {
|
||||
$rankScore += $this->getScoreByCandidateRanking($CandidatesRanked++, $election);
|
||||
}
|
||||
|
||||
foreach ($oneRank as $oneCandidateInRank) {
|
||||
$score[$election->getCandidateKey($oneCandidateInRank)] += ($rankScore / \count($oneRank)) * $oneVote->getWeight($election);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array_walk($score, static fn (float &$sc): float => $sc = round($sc, self::DECIMAL_PRECISION));
|
||||
ksort($score, \SORT_NATURAL);
|
||||
arsort($score, \SORT_NUMERIC);
|
||||
|
||||
$rank = 0;
|
||||
$lastScore = null;
|
||||
$result = [];
|
||||
foreach ($score as $candidateKey => $candidateScore) {
|
||||
if ($candidateScore === $lastScore) {
|
||||
$result[$rank][] = $candidateKey;
|
||||
} else {
|
||||
$result[++$rank] = [$candidateKey];
|
||||
$lastScore = $candidateScore;
|
||||
}
|
||||
}
|
||||
|
||||
$this->Stats = $score;
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
|
||||
protected function getScoreByCandidateRanking(int $CandidatesRanked, Election $election): float
|
||||
{
|
||||
return (float) ($election->countCandidates() + static::$optionStarting - 1 - $CandidatesRanked);
|
||||
}
|
||||
}
|
27
include/condorcet/src/Algo/Methods/Borda/DowdallSystem.php
Normal file
27
include/condorcet/src/Algo/Methods/Borda/DowdallSystem.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/*
|
||||
Part of BORDA COUNT method Module - From the original Condorcet 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\Algo\Methods\Borda;
|
||||
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class DowdallSystem extends BordaCount
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['DowdallSystem', 'Dowdall System', 'Nauru', 'Borda Nauru'];
|
||||
|
||||
protected function getScoreByCandidateRanking(int $CandidatesRanked, Election $election): float
|
||||
{
|
||||
return (float) (1 / ($CandidatesRanked + 1));
|
||||
}
|
||||
}
|
102
include/condorcet/src/Algo/Methods/CondorcetBasic.php
Normal file
102
include/condorcet/src/Algo/Methods/CondorcetBasic.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?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\Algo\Methods;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
use CondorcetPHP\Condorcet\Throwable\AlgorithmWithoutRankingFeatureException;
|
||||
|
||||
// Condorcet Basic Class, provide natural Condorcet winner or looser
|
||||
class CondorcetBasic extends Method implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['CondorcetBasic'];
|
||||
|
||||
// Basic Condorcet
|
||||
protected ?int $CondorcetWinner = null;
|
||||
protected ?int $CondorcetLoser = null;
|
||||
|
||||
|
||||
/////////// PUBLIC ///////////
|
||||
|
||||
|
||||
public function getResult(): never
|
||||
{
|
||||
throw new AlgorithmWithoutRankingFeatureException(self::METHOD_NAME[0]);
|
||||
}
|
||||
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Get a Condorcet certified winner. If there is none = null. You can force a winner choice with alternative supported methods ($substitution)
|
||||
public function getWinner(): ?int
|
||||
{
|
||||
// Cache
|
||||
if ($this->CondorcetWinner !== null) {
|
||||
return $this->CondorcetWinner;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// Basic Condorcet calculation
|
||||
foreach ($this->getElection()->getPairwise() as $candidate_key => $candidat_detail) {
|
||||
$winner = true;
|
||||
|
||||
foreach ($candidat_detail['win'] as $challenger_key => $win_count) {
|
||||
if ($win_count <= $candidat_detail['lose'][$challenger_key]) {
|
||||
$winner = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($winner) {
|
||||
$this->CondorcetWinner = $candidate_key;
|
||||
return $this->CondorcetWinner;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get a Condorcet certified loser. If there is none = null. You can force a winner choice with alternative supported methods ($substitution)
|
||||
public function getLoser(): ?int
|
||||
{
|
||||
// Cache
|
||||
if ($this->CondorcetLoser !== null) {
|
||||
return $this->CondorcetLoser;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// Basic Condorcet calculation
|
||||
foreach ($this->getElection()->getPairwise() as $candidate_key => $candidat_detail) {
|
||||
$loser = true;
|
||||
|
||||
foreach ($candidat_detail['lose'] as $challenger_key => $lose_count) {
|
||||
if ($lose_count <= $candidat_detail['win'][$challenger_key]) {
|
||||
$loser = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($loser) {
|
||||
$this->CondorcetLoser = $candidate_key;
|
||||
return $this->CondorcetLoser;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
35
include/condorcet/src/Algo/Methods/Copeland/Copeland.php
Normal file
35
include/condorcet/src/Algo/Methods/Copeland/Copeland.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/*
|
||||
Part of COPELAND method Module - From the original Condorcet 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\Algo\Methods\Copeland;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\PairwiseStatsBased_Core;
|
||||
|
||||
// Copeland is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Copeland_method
|
||||
class Copeland extends PairwiseStatsBased_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Copeland'];
|
||||
|
||||
protected const COUNT_TYPE = 'balance';
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: COPELAND ALGORITHM. :://
|
||||
|
||||
protected function looking(array $challenge): int
|
||||
{
|
||||
return max($challenge);
|
||||
}
|
||||
}
|
90
include/condorcet/src/Algo/Methods/Dodgson/DodgsonQuick.php
Normal file
90
include/condorcet/src/Algo/Methods/Dodgson/DodgsonQuick.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/*
|
||||
Part of DODGSON QUICK method Module - From the original Condorcet 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\Algo\Methods\Dodgson;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
|
||||
// DODGSON Quick is an approximation for Dodgson method | https://www.maa.org/sites/default/files/pdf/cmj_ftp/CMJ/September%202010/3%20Articles/6%2009-229%20Ratliff/Dodgson_CMJ_Final.pdf
|
||||
class DodgsonQuick extends Method implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Dodgson Quick', 'DodgsonQuick', 'Dodgson Quick Winner'];
|
||||
|
||||
protected ?array $Stats = null;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$stats = [];
|
||||
|
||||
foreach ($this->Stats as $candidateKey => $dodgsonQuickValue) {
|
||||
$stats[(string) $election->getCandidateObjectFromKey($candidateKey)] = $dodgsonQuickValue;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: DODGSON ALGORITHM. :://
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$pairwise = $election->getPairwise();
|
||||
$HeadToHead = [];
|
||||
|
||||
foreach ($pairwise as $candidateId => $CandidateStats) {
|
||||
foreach ($CandidateStats['lose'] as $opponentId => $CandidateLose) {
|
||||
if (($diff = $CandidateLose - $CandidateStats['win'][$opponentId]) >= 0) {
|
||||
$HeadToHead[$candidateId][$opponentId] = $diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dodgsonQuick = [];
|
||||
|
||||
foreach ($HeadToHead as $candidateId => $CandidateTidemanScores) {
|
||||
$dodgsonQuick[$candidateId] = 0;
|
||||
|
||||
foreach ($CandidateTidemanScores as $oneTidemanScore) {
|
||||
$dodgsonQuick[$candidateId] += ceil($oneTidemanScore / 2);
|
||||
}
|
||||
}
|
||||
asort($dodgsonQuick);
|
||||
|
||||
$rank = 0;
|
||||
$result = [];
|
||||
|
||||
if ($basicCondorcetWinner = $election->getCondorcetWinner()) {
|
||||
$result[++$rank][] = $election->getCandidateKey($basicCondorcetWinner);
|
||||
}
|
||||
|
||||
$lastDodgsonQuickValue = null;
|
||||
|
||||
foreach ($dodgsonQuick as $CandidateId => $dodgsonQuickValue) {
|
||||
if ($lastDodgsonQuickValue === $dodgsonQuickValue) {
|
||||
$result[$rank][] = $CandidateId;
|
||||
} else {
|
||||
$result[++$rank][] = $CandidateId;
|
||||
$lastDodgsonQuickValue = $dodgsonQuickValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->Stats = $dodgsonQuick;
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*
|
||||
Part of DODGSON TIDEMAN APPROXIMATION method Module - From the original Condorcet 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\Algo\Methods\Dodgson;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\MethodInterface;
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\PairwiseStatsBased_Core;
|
||||
|
||||
// DODGSON Tideman is an approximation for Dodgson method | https://www.maa.org/sites/default/files/pdf/cmj_ftp/CMJ/September%202010/3%20Articles/6%2009-229%20Ratliff/Dodgson_CMJ_Final.pdf
|
||||
class DodgsonTidemanApproximation extends PairwiseStatsBased_Core implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Dodgson Tideman Approximation', 'DodgsonTidemanApproximation', 'Dodgson Tideman', 'DodgsonTideman'];
|
||||
|
||||
protected const COUNT_TYPE = 'sum_defeat_margin';
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: DODGSON ALGORITHM. :://
|
||||
|
||||
protected function looking(array $challenge): int
|
||||
{
|
||||
return min($challenge);
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/*
|
||||
Part of Highest Averages Methods module - From the original Condorcet 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\Algo\Methods\HighestAverages;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface, StatsVerbosity};
|
||||
|
||||
abstract class HighestAverages_Core extends Method implements MethodInterface
|
||||
{
|
||||
final public const IS_PROPORTIONAL = true;
|
||||
|
||||
protected array $candidatesVotes = [];
|
||||
protected array $candidatesSeats = [];
|
||||
protected array $rounds = [];
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$this->countVotesPerCandidates();
|
||||
|
||||
if (array_sum($this->candidatesVotes) > 0) {
|
||||
foreach (array_keys($this->getElection()->getCandidatesList()) as $candidateKey) {
|
||||
$this->candidatesSeats[$candidateKey] = 0;
|
||||
}
|
||||
|
||||
# Rounds
|
||||
$this->Result = $this->createResult($this->makeRounds());
|
||||
} else {
|
||||
$this->Result = $this->createResult([]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function countVotesPerCandidates(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
foreach (array_keys($election->getCandidatesList()) as $candidateKey) {
|
||||
$this->candidatesVotes[$candidateKey] = 0;
|
||||
}
|
||||
|
||||
foreach ($election->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
if ($oneVote->countRankingCandidates() === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$voteWinnerRank = $oneVote->getContextualRankingWithoutSort($election)[1];
|
||||
|
||||
if (\count($voteWinnerRank) !== 1) {
|
||||
continue;
|
||||
} // This method support only one winner per vote. Ignore bad votes.
|
||||
|
||||
$this->candidatesVotes[$election->getCandidateKey(reset($voteWinnerRank))] += $oneVote->getWeight($election);
|
||||
}
|
||||
}
|
||||
|
||||
protected function makeRounds(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$results = [];
|
||||
|
||||
while (array_sum($this->candidatesSeats) < $election->getNumberOfSeats()) {
|
||||
$roundNumber = \count($this->rounds) + 1;
|
||||
$maxQuotient = null;
|
||||
$maxQuotientCandidateKey = null;
|
||||
|
||||
foreach ($this->candidatesVotes as $candidateKey => $oneCandidateVotes) {
|
||||
$quotient = $this->computeQuotient($oneCandidateVotes, $this->candidatesSeats[$candidateKey]);
|
||||
|
||||
$this->rounds[$roundNumber][$candidateKey]['Quotient'] = $quotient;
|
||||
$this->rounds[$roundNumber][$candidateKey]['NumberOfSeatsAllocatedBeforeRound'] = $this->candidatesSeats[$candidateKey];
|
||||
|
||||
if ($quotient > $maxQuotient) {
|
||||
$maxQuotient = $quotient;
|
||||
$maxQuotientCandidateKey = $candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
$this->candidatesSeats[$maxQuotientCandidateKey]++;
|
||||
$results[$roundNumber] = $maxQuotientCandidateKey;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
abstract protected function computeQuotient(int $votesWeight, int $seats): float;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$stats = [];
|
||||
|
||||
if ($election->getStatsVerbosity()->value > StatsVerbosity::LOW->value) {
|
||||
foreach ($this->rounds as $roundNumber => $oneRound) {
|
||||
foreach ($oneRound as $candidateKey => $roundCandidateStats) {
|
||||
$stats['Rounds'][$roundNumber][$election->getCandidateObjectFromKey($candidateKey)->getName()] = $roundCandidateStats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($election->getStatsVerbosity()->value > StatsVerbosity::NONE->value) {
|
||||
foreach ($this->candidatesSeats as $candidateKey => $candidateSeats) {
|
||||
$stats['Seats per Candidates'][$election->getCandidateObjectFromKey($candidateKey)->getName()] = $candidateSeats;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*
|
||||
Part of Highest Averages Methods module - From the original Condorcet 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\Algo\Methods\HighestAverages;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{MethodInterface};
|
||||
|
||||
# Jefferson is a proportional algorithm | https://en.wikipedia.org/wiki/D%27Hondt_method
|
||||
class Jefferson extends HighestAverages_Core implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Jefferson', 'D\'Hondt', 'Thomas Jefferson'];
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
protected function computeQuotient(int $votesWeight, int $seats): float
|
||||
{
|
||||
return (float) ($votesWeight / ($seats + 1));
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
Part of Highest Averages Methods module - From the original Condorcet 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\Algo\Methods\HighestAverages;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{MethodInterface};
|
||||
|
||||
# Copeland is a proportional algorithm | https://en.wikipedia.org/wiki/Webster/Sainte-Lagu%C3%AB_method
|
||||
class SainteLague extends HighestAverages_Core implements MethodInterface
|
||||
{
|
||||
public static int|float $optionFirstDivisor = 1;
|
||||
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Sainte-Laguë', 'SainteLague', 'Webster', 'Major Fractions Method'];
|
||||
|
||||
protected function computeQuotient(int $votesWeight, int $seats): float
|
||||
{
|
||||
$divisor = ($seats !== 0) ? ($seats * 2 + 1) : self::$optionFirstDivisor;
|
||||
|
||||
return (float) ($votesWeight / $divisor);
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/*
|
||||
Part of INSTANT-RUNOFF method Module - From the original Condorcet 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\Algo\Methods\InstantRunoff;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\TieBreakersCollection;
|
||||
|
||||
class InstantRunoff extends Method implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Instant-runoff', 'InstantRunoff', 'IRV', 'preferential voting', 'ranked-choice voting', 'alternative vote', 'AlternativeVote', 'transferable vote', 'Vote alternatif'];
|
||||
|
||||
protected ?array $Stats = null;
|
||||
|
||||
public readonly float $majority;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$stats = ['majority' => $this->majority];
|
||||
|
||||
foreach ($this->Stats as $oneIterationKey => $oneIterationData) {
|
||||
if (\count($oneIterationData) === 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($oneIterationData as $candidateKey => $candidateValue) {
|
||||
$stats['rounds'][$oneIterationKey][(string) $election->getCandidateObjectFromKey($candidateKey)] = $candidateValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: Alternative Vote ALGORITHM. :://
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$candidateCount = $election->countCandidates();
|
||||
$this->majority = $election->sumValidVotesWeightWithConstraints() / 2;
|
||||
|
||||
$candidateDone = [];
|
||||
$result = [];
|
||||
$CandidatesWinnerCount = 0;
|
||||
$CandidatesLoserCount = 0;
|
||||
|
||||
$iteration = 0;
|
||||
|
||||
while (\count($candidateDone) < $candidateCount) {
|
||||
$score = $this->makeScore($candidateDone);
|
||||
$maxScore = max($score);
|
||||
$minScore = min($score);
|
||||
|
||||
$this->Stats[++$iteration] = $score;
|
||||
|
||||
if ($maxScore > $this->majority) {
|
||||
foreach ($score as $candidateKey => $candidateScore) {
|
||||
if ($candidateScore !== $maxScore) {
|
||||
continue;
|
||||
} else {
|
||||
$candidateDone[] = $candidateKey;
|
||||
$result[++$CandidatesWinnerCount][] = $candidateKey;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$LosersToRegister = [];
|
||||
|
||||
foreach ($score as $candidateKey => $candidateScore) {
|
||||
if ($candidateScore !== $minScore) {
|
||||
continue;
|
||||
} else {
|
||||
$LosersToRegister[] = $candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Tie Breaking
|
||||
$round = \count($LosersToRegister);
|
||||
for ($i = 1; $i < $round; $i++) { // A little silly. But ultimately shorter and simpler.
|
||||
$LosersToRegister = TieBreakersCollection::electSomeLosersbasedOnPairwiseComparaison($election, $LosersToRegister);
|
||||
}
|
||||
|
||||
$CandidatesLoserCount += \count($LosersToRegister);
|
||||
array_push($candidateDone, ...$LosersToRegister);
|
||||
$result[$candidateCount - $CandidatesLoserCount + 1] = $LosersToRegister;
|
||||
}
|
||||
}
|
||||
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
|
||||
protected function makeScore(array $candidateDone): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$score = [];
|
||||
|
||||
foreach ($election->getCandidatesList() as $candidateKey => $oneCandidate) {
|
||||
if (!\in_array(needle: $candidateKey, haystack: $candidateDone, strict: true)) {
|
||||
$score[$candidateKey] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($election->getVotesManager()->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
$weight = $oneVote->getWeight($election);
|
||||
|
||||
foreach ($oneVote->getContextualRankingWithoutSort($election) as $oneRank) {
|
||||
foreach ($oneRank as $oneCandidate) {
|
||||
if (\count($oneRank) !== 1) {
|
||||
break;
|
||||
} elseif (!\in_array(needle: ($candidateKey = $election->getCandidateKey($oneCandidate)), haystack: $candidateDone, strict: true)) {
|
||||
$score[$candidateKey] += $weight;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
}
|
172
include/condorcet/src/Algo/Methods/KemenyYoung/KemenyYoung.php
Normal file
172
include/condorcet/src/Algo/Methods/KemenyYoung/KemenyYoung.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/*
|
||||
Part of KEMENY–YOUNG method Module - From the original Condorcet 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\Algo\Methods\KemenyYoung;
|
||||
|
||||
use CondorcetPHP\Condorcet\Result;
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface, Pairwise, StatsVerbosity};
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\Permutations;
|
||||
|
||||
// Kemeny-Young is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Kemeny%E2%80%93Young_method
|
||||
class KemenyYoung extends Method implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Kemeny–Young', 'Kemeny-Young', 'Kemeny Young', 'KemenyYoung', 'Kemeny rule', 'VoteFair popularity ranking', 'Maximum Likelihood Method', 'Median Relation'];
|
||||
|
||||
// Method Name
|
||||
final public const CONFLICT_WARNING_CODE = 42;
|
||||
|
||||
// Limits
|
||||
# If you need 9 candidates, you must use \ini_set('memory_limit','1024M'); before. Do not try to go to 10, it is not viable!
|
||||
public static ?int $MaxCandidates = 10;
|
||||
|
||||
// Cache process
|
||||
protected readonly int $countElectionCandidates;
|
||||
protected readonly array $candidatesKey;
|
||||
protected readonly int $countPossibleRanking;
|
||||
|
||||
// processing
|
||||
protected int $MaxScore = -1;
|
||||
protected int $Conflits = 0;
|
||||
protected int $bestRankingKey;
|
||||
protected array $bestRankingTab;
|
||||
|
||||
|
||||
/////////// PUBLIC ///////////
|
||||
|
||||
|
||||
// Get the Kemeny ranking
|
||||
public function getResult(): Result
|
||||
{
|
||||
// Cache
|
||||
if ($this->Result === null) {
|
||||
$this->countElectionCandidates = $this->getElection()->countCandidates();
|
||||
$this->candidatesKey = array_keys($this->getElection()->getCandidatesList());
|
||||
$this->countPossibleRanking = Permutations::getPossibleCountOfPermutations($this->countElectionCandidates);
|
||||
|
||||
$this->computeMaxAndConflicts();
|
||||
$this->makeRanking();
|
||||
$this->conflictInfos();
|
||||
}
|
||||
|
||||
// Return
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$stats = [];
|
||||
|
||||
$stats['Best Score'] = $this->MaxScore;
|
||||
$stats['Ranking In Conflicts'] = $this->Conflits > 0 ? $this->Conflits + 1 : $this->Conflits;
|
||||
|
||||
if ($election->getStatsVerbosity()->value >= StatsVerbosity::FULL->value) {
|
||||
$explicit = [];
|
||||
|
||||
foreach ($this->getPossibleRankingIterator() as $key => $value) {
|
||||
// Human readable
|
||||
$i = 1;
|
||||
foreach ($value as $candidate_key) {
|
||||
$explicit[$key][$i++] = $election->getCandidateObjectFromKey($candidate_key)->getName();
|
||||
}
|
||||
|
||||
$explicit[$key]['score'] = $this->computeOneScore($value, $election->getPairwise());
|
||||
}
|
||||
|
||||
$stats['Ranking Scores'] = $explicit;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function conflictInfos(): void
|
||||
{
|
||||
if ($this->Conflits > 0) {
|
||||
$this->Result->addWarning(
|
||||
type: self::CONFLICT_WARNING_CODE,
|
||||
msg: ($this->Conflits + 1).';'.$this->MaxScore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
|
||||
//:: Kemeny-Young ALGORITHM. :://
|
||||
|
||||
protected function getPossibleRankingIterator(): \Generator
|
||||
{
|
||||
$perm = new Permutations($this->candidatesKey);
|
||||
|
||||
$key = 0;
|
||||
foreach ($perm->getPermutationGenerator() as $onePermutation) {
|
||||
yield $key++ => $onePermutation;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function computeMaxAndConflicts(): void
|
||||
{
|
||||
$pairwise = $this->getElection()->getPairwise();
|
||||
|
||||
foreach ($this->getPossibleRankingIterator() as $keyScore => $onePossibleRanking) {
|
||||
$rankingScore = $this->computeOneScore($onePossibleRanking, $pairwise);
|
||||
|
||||
// Max Ranking Score
|
||||
if ($rankingScore > $this->MaxScore) {
|
||||
$this->MaxScore = $rankingScore;
|
||||
$this->Conflits = 0;
|
||||
$this->bestRankingKey = $keyScore;
|
||||
$this->bestRankingTab = $onePossibleRanking;
|
||||
} elseif ($rankingScore === $this->MaxScore) {
|
||||
$this->Conflits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function computeOneScore(array $ranking, Pairwise $pairwise): int
|
||||
{
|
||||
$rankingScore = 0;
|
||||
$do = [];
|
||||
|
||||
foreach ($ranking as $candidateId) {
|
||||
$do[] = $candidateId;
|
||||
|
||||
foreach ($ranking as $rankCandidate) {
|
||||
if (!\in_array(needle: $rankCandidate, haystack: $do, strict: true)) {
|
||||
$rankingScore += $pairwise[$candidateId]['win'][$rankCandidate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rankingScore;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
I do not know how in the very unlikely event that several possible classifications have the same highest score.
|
||||
In the current state, one of them is chosen arbitrarily.
|
||||
|
||||
See issue on Github : https://github.com/julien-boudry/Condorcet/issues/6
|
||||
*/
|
||||
protected function makeRanking(): void
|
||||
{
|
||||
$winnerRanking = [null, ...$this->bestRankingTab];
|
||||
unset($winnerRanking[0]);
|
||||
|
||||
$this->Result = $this->createResult($winnerRanking);
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/*
|
||||
Part of Largest Remainder Methods module - From the original Condorcet 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\Algo\Methods\LargestRemainder;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{MethodInterface};
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\HighestAverages\HighestAverages_Core;
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\StvQuotas;
|
||||
|
||||
# Largest Remainder is a proportional algorithm | https://en.wikipedia.org/wiki/Largest_remainder_method
|
||||
class LargestRemainder extends HighestAverages_Core implements MethodInterface
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Largest Remainder', 'LargestRemainder', 'LR', 'Hare–Niemeyer method', 'Hamilton method', 'Vinton\'s method'];
|
||||
|
||||
public static StvQuotas $optionQuota = StvQuotas::HARE;
|
||||
|
||||
protected function makeRounds(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$results = [];
|
||||
$rescueCandidatesKeys = array_keys($election->getCandidatesList());
|
||||
reset($rescueCandidatesKeys);
|
||||
|
||||
$quotient = $this->computeQuotient($election->sumValidVotesWeightWithConstraints(), $election->getNumberOfSeats());
|
||||
|
||||
while (array_sum($this->candidatesSeats) < $election->getNumberOfSeats()) {
|
||||
$roundNumber = \count($this->rounds) + 1;
|
||||
$maxVotes = null;
|
||||
$maxVotesCandidateKey = null;
|
||||
|
||||
foreach ($this->candidatesVotes as $candidateKey => $oneCandidateVotes) {
|
||||
$this->rounds[$roundNumber][$candidateKey]['NumberOfVotesAllocatedBeforeRound'] = $oneCandidateVotes;
|
||||
$this->rounds[$roundNumber][$candidateKey]['NumberOfSeatsAllocatedBeforeRound'] = $this->candidatesSeats[$candidateKey];
|
||||
|
||||
if ($oneCandidateVotes > $maxVotes) {
|
||||
$maxVotes = $oneCandidateVotes;
|
||||
$maxVotesCandidateKey = $candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
if ($maxVotesCandidateKey === null) {
|
||||
$n = current($rescueCandidatesKeys);
|
||||
$maxVotesCandidateKey = $n;
|
||||
next($rescueCandidatesKeys) !== false || reset($rescueCandidatesKeys);
|
||||
}
|
||||
|
||||
$this->candidatesVotes[$maxVotesCandidateKey] -= $quotient;
|
||||
$this->candidatesSeats[$maxVotesCandidateKey]++;
|
||||
$results[$roundNumber] = $maxVotesCandidateKey;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function computeQuotient(int $votesWeight, int $seats): float
|
||||
{
|
||||
return self::$optionQuota->getQuota($votesWeight, $seats);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/*
|
||||
Part of FTPT method Module - From the original Condorcet 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\Algo\Methods\Majority;
|
||||
|
||||
class FirstPastThePost extends Majority_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['First-past-the-post voting', 'First-past-the-post', 'First Choice', 'FirstChoice', 'FPTP', 'FPP', 'SMP', 'FTPT'];
|
||||
|
||||
// Mod
|
||||
protected int $maxRound = 1;
|
||||
protected int $targetNumberOfCandidatesForTheNextRound = 2;
|
||||
protected int $numberOfTargetedCandidatesAfterEachRound = 0;
|
||||
}
|
161
include/condorcet/src/Algo/Methods/Majority/Majority_Core.php
Normal file
161
include/condorcet/src/Algo/Methods/Majority/Majority_Core.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/*
|
||||
Part of FTPT method Module - From the original Condorcet 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\Algo\Methods\Majority;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
|
||||
abstract class Majority_Core extends Method implements MethodInterface
|
||||
{
|
||||
protected int $maxRound;
|
||||
protected int $targetNumberOfCandidatesForTheNextRound;
|
||||
protected int $numberOfTargetedCandidatesAfterEachRound;
|
||||
|
||||
protected array $admittedCandidates = [];
|
||||
protected ?array $Stats = null;
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$stats = [];
|
||||
|
||||
foreach ($this->Stats as $roundNumber => $roundScore) {
|
||||
foreach ($roundScore as $candidateKey => $oneScore) {
|
||||
$stats[$roundNumber][(string) $election->getCandidateObjectFromKey($candidateKey)] = $oneScore;
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$round = 1;
|
||||
$resolved = false;
|
||||
$score = [];
|
||||
|
||||
// Start a round
|
||||
while ($resolved === false) {
|
||||
$roundScore = $this->doOneRound();
|
||||
ksort($roundScore, \SORT_NATURAL);
|
||||
arsort($roundScore, \SORT_NUMERIC);
|
||||
|
||||
$score[$round] = $roundScore;
|
||||
|
||||
if ($round === 1) {
|
||||
foreach (array_keys($election->getCandidatesList()) as $oneCandidateKey) {
|
||||
$score[$round][$oneCandidateKey] ??= 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
if ($round === $this->maxRound || reset($roundScore) > (array_sum($roundScore) / 2)) {
|
||||
$resolved = true;
|
||||
|
||||
if (isset($score[$round - 1]) && $score[$round] === $score[$round - 1]) {
|
||||
unset($score[$round]);
|
||||
}
|
||||
} else {
|
||||
$lastScore = null;
|
||||
$nextRoundAddedCandidates = 0;
|
||||
|
||||
$this->admittedCandidates = [];
|
||||
|
||||
foreach ($roundScore as $oneCandidateKey => $oneScore) {
|
||||
if ($lastScore === null ||
|
||||
$nextRoundAddedCandidates < ($this->targetNumberOfCandidatesForTheNextRound + ($this->numberOfTargetedCandidatesAfterEachRound * ($round - 1))) ||
|
||||
$oneScore === $lastScore
|
||||
) {
|
||||
$this->admittedCandidates[] = $oneCandidateKey;
|
||||
$lastScore = $oneScore;
|
||||
$nextRoundAddedCandidates++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$round++;
|
||||
}
|
||||
|
||||
// Compute Ranking
|
||||
$rank = 0;
|
||||
$result = [];
|
||||
krsort($score, \SORT_NUMERIC);
|
||||
$doneCandidates = [];
|
||||
|
||||
foreach ($score as $oneRound) {
|
||||
$lastScore = null;
|
||||
foreach ($oneRound as $candidateKey => $candidateScore) {
|
||||
if (!\in_array(needle: $candidateKey, haystack: $doneCandidates, strict: true)) {
|
||||
if ($candidateScore === $lastScore) {
|
||||
$doneCandidates[] = $result[$rank][] = $candidateKey;
|
||||
} else {
|
||||
$result[++$rank] = [$doneCandidates[] = $candidateKey];
|
||||
$lastScore = $candidateScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finalizing
|
||||
ksort($score, \SORT_NUMERIC);
|
||||
$this->Stats = $score;
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
|
||||
protected function doOneRound(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$roundScore = [];
|
||||
|
||||
foreach ($election->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
$weight = $oneVote->getWeight($election);
|
||||
|
||||
$oneRanking = $oneVote->getContextualRankingWithoutSort($election);
|
||||
|
||||
if (!empty($this->admittedCandidates)) {
|
||||
foreach ($oneRanking as $rankKey => $oneRank) {
|
||||
foreach ($oneRank as $InRankKey => $oneCandidate) {
|
||||
if (!\in_array(needle: $election->getCandidateKey($oneCandidate), haystack: $this->admittedCandidates, strict: true)) {
|
||||
unset($oneRanking[$rankKey][$InRankKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($oneRanking[$rankKey])) {
|
||||
unset($oneRanking[$rankKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (($newFirstRank = reset($oneRanking)) !== false) {
|
||||
$oneRanking = [1 => $newFirstRank];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($oneRanking[1])) {
|
||||
foreach ($oneRanking[1] as $oneCandidateInRank) {
|
||||
$roundScore[$election->getCandidateKey($oneCandidateInRank)] ??= (float) 0;
|
||||
$roundScore[$election->getCandidateKey($oneCandidateInRank)] += (1 / \count($oneRanking[1])) * $weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
array_walk($roundScore, static fn (float &$sc): float => $sc = round($sc, self::DECIMAL_PRECISION));
|
||||
|
||||
return $roundScore;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/*
|
||||
Part of FTPT method Module - From the original Condorcet 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\Algo\Methods\Majority;
|
||||
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class MultipleRoundsSystem extends Majority_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Multiple Rounds System', 'MultipleRoundsSystem', 'Multiple Rounds', 'Majority', 'Majority System', 'Two-round system', 'second ballot', 'runoff voting', 'ballotage', 'two round system', 'two round', 'two rounds', 'two rounds system', 'runoff voting'];
|
||||
|
||||
// Mod
|
||||
protected static int $optionMAX_ROUND = 2;
|
||||
protected static int $optionTARGET_NUMBER_OF_CANDIDATES_FOR_THE_NEXT_ROUND = 2;
|
||||
protected static int $optionNUMBER_OF_TARGETED_CANDIDATES_AFTER_EACH_ROUND = 0;
|
||||
|
||||
public function __construct(Election $mother)
|
||||
{
|
||||
$this->maxRound = self::$optionMAX_ROUND;
|
||||
$this->targetNumberOfCandidatesForTheNextRound = self::$optionTARGET_NUMBER_OF_CANDIDATES_FOR_THE_NEXT_ROUND;
|
||||
$this->numberOfTargetedCandidatesAfterEachRound = self::$optionNUMBER_OF_TARGETED_CANDIDATES_AFTER_EACH_ROUND;
|
||||
|
||||
parent::__construct($mother);
|
||||
}
|
||||
}
|
35
include/condorcet/src/Algo/Methods/Minimax/MinimaxMargin.php
Normal file
35
include/condorcet/src/Algo/Methods/Minimax/MinimaxMargin.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/*
|
||||
Part of MINIMAX method Module - From the original Condorcet 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\Algo\Methods\Minimax;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\PairwiseStatsBased_Core;
|
||||
|
||||
// Minimax is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Schulze_method
|
||||
class MinimaxMargin extends PairwiseStatsBased_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Minimax Margin', 'MinimaxMargin', 'MinimaxMargin', 'Minimax_Margin'];
|
||||
|
||||
protected const COUNT_TYPE = 'worst_pairwise_defeat_margin';
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: SIMPSON ALGORITHM. :://
|
||||
|
||||
protected function looking(array $challenge): int
|
||||
{
|
||||
return min($challenge);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/*
|
||||
Part of MINIMAX method Module - From the original Condorcet 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\Algo\Methods\Minimax;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\PairwiseStatsBased_Core;
|
||||
|
||||
// Minimax is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Schulze_method
|
||||
class MinimaxOpposition extends PairwiseStatsBased_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Minimax Opposition', 'MinimaxOpposition', 'Minimax_Opposition'];
|
||||
|
||||
protected const COUNT_TYPE = 'worst_pairwise_opposition';
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: SIMPSON ALGORITHM. :://
|
||||
|
||||
protected function looking(array $challenge): int
|
||||
{
|
||||
return min($challenge);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/*
|
||||
Part of MINIMAX method Module - From the original Condorcet 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\Algo\Methods\Minimax;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\PairwiseStatsBased_Core;
|
||||
|
||||
// Minimax is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Schulze_method
|
||||
class MinimaxWinning extends PairwiseStatsBased_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Minimax Winning', 'MinimaxWinning', 'Minimax', 'Minimax_Winning', 'Simpson', 'Simpson-Kramer', 'Simpson-Kramer Method', 'Simpson Method'];
|
||||
|
||||
protected const COUNT_TYPE = 'worst_pairwise_defeat_winning';
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
//:: SIMPSON ALGORITHM. :://
|
||||
|
||||
protected function looking(array $challenge): int
|
||||
{
|
||||
return min($challenge);
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?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\Algo\Methods;
|
||||
|
||||
use CondorcetPHP\Condorcet\Result;
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\PairwiseStats;
|
||||
|
||||
abstract class PairwiseStatsBased_Core extends Method implements MethodInterface
|
||||
{
|
||||
private const COUNT_TYPE = 'abstractCountType';
|
||||
|
||||
protected readonly array $Comparison;
|
||||
|
||||
/////////// PUBLIC ///////////
|
||||
|
||||
|
||||
// Get the ranking
|
||||
public function getResult(): Result
|
||||
{
|
||||
// Cache
|
||||
if ($this->Result !== null) {
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// Comparison calculation
|
||||
$this->Comparison = PairwiseStats::PairwiseComparison($this->getElection()->getPairwise());
|
||||
|
||||
// Ranking calculation
|
||||
$this->makeRanking();
|
||||
|
||||
// Return
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
|
||||
// Get the stats
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$explicit = [];
|
||||
|
||||
foreach ($this->Comparison as $candidate_key => $value) {
|
||||
$explicit[$election->getCandidateObjectFromKey($candidate_key)->getName()] = [static::COUNT_TYPE => $value[static::COUNT_TYPE]];
|
||||
}
|
||||
|
||||
return $explicit;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
|
||||
//:: ALGORITHM. :://
|
||||
|
||||
protected function makeRanking(): void
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// Calculate ranking
|
||||
$challenge = [];
|
||||
$rank = 1;
|
||||
$done = 0;
|
||||
|
||||
foreach ($this->Comparison as $candidate_key => $candidate_data) {
|
||||
$challenge[$candidate_key] = $candidate_data[static::COUNT_TYPE];
|
||||
}
|
||||
|
||||
while ($done < $this->getElection()->countCandidates()) {
|
||||
$looking = $this->looking($challenge);
|
||||
|
||||
foreach ($challenge as $candidate => $value) {
|
||||
if ($value === $looking) {
|
||||
$result[$rank][] = $candidate;
|
||||
|
||||
$done++;
|
||||
unset($challenge[$candidate]);
|
||||
}
|
||||
}
|
||||
|
||||
$rank++;
|
||||
}
|
||||
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
|
||||
abstract protected function looking(array $challenge): int;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/*
|
||||
Part of RANKED PAIRS method Module - From the original Condorcet 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\Algo\Methods\RankedPairs;
|
||||
|
||||
// Ranked Pairs is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Ranked_Pairs
|
||||
class RankedPairsMargin extends RankedPairs_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Ranked Pairs Margin', 'RankedPairsMargin', 'Tideman Margin', 'RP Margin', 'Ranked Pairs', 'RankedPairs', 'Tideman method'];
|
||||
|
||||
protected const RP_VARIANT_1 = 'margin';
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/*
|
||||
Part of RANKED PAIRS method Module - From the original Condorcet 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\Algo\Methods\RankedPairs;
|
||||
|
||||
// Ranked Pairs is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Ranked_Pairs
|
||||
class RankedPairsWinning extends RankedPairs_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Ranked Pairs Winning', 'RankedPairsWinning', 'Tideman Winning', 'RP Winning'];
|
||||
|
||||
protected const RP_VARIANT_1 = 'win';
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
<?php
|
||||
/*
|
||||
Part of RANKED PAIRS method Module - From the original Condorcet 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\Algo\Methods\RankedPairs;
|
||||
|
||||
use CondorcetPHP\Condorcet\Result;
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
|
||||
// Ranked Pairs is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Ranked_Pairs
|
||||
abstract class RankedPairs_Core extends Method implements MethodInterface
|
||||
{
|
||||
protected const RP_VARIANT_1 = 'abstractVariant';
|
||||
|
||||
// Limits
|
||||
public static ?int $MaxCandidates = 60;
|
||||
|
||||
// Ranked Pairs
|
||||
protected readonly array $PairwiseSort;
|
||||
protected array $Arcs = [];
|
||||
protected ?array $Stats = null;
|
||||
protected bool $StatsDone = false;
|
||||
|
||||
|
||||
/////////// PUBLIC ///////////
|
||||
|
||||
|
||||
// Get the Ranked Pairs ranking
|
||||
public function getResult(): Result
|
||||
{
|
||||
// Cache
|
||||
if ($this->Result !== null) {
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// Sort pairwise
|
||||
$this->PairwiseSort = $this->pairwiseSort();
|
||||
|
||||
// Ranking calculation
|
||||
$this->makeArcs();
|
||||
|
||||
// Make Stats
|
||||
$this->Stats['tally'] = $this->PairwiseSort;
|
||||
$this->Stats['arcs'] = $this->Arcs;
|
||||
|
||||
// Make Result
|
||||
return $this->Result = $this->createResult($this->makeResult());
|
||||
}
|
||||
|
||||
// Get the Ranked Pair ranking
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
if (!$this->StatsDone) {
|
||||
foreach ($this->Stats['tally'] as &$Roundvalue) {
|
||||
foreach ($Roundvalue as &$Arcvalue) {
|
||||
foreach ($Arcvalue as $key => &$value) {
|
||||
if ($key === 'from' || $key === 'to') {
|
||||
$value = $election->getCandidateObjectFromKey($value)->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->Stats['arcs'] as &$Arcvalue) {
|
||||
foreach ($Arcvalue as $key => &$value) {
|
||||
if ($key === 'from' || $key === 'to') {
|
||||
$value = $election->getCandidateObjectFromKey($value)->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->StatsDone = true;
|
||||
}
|
||||
|
||||
return $this->Stats;
|
||||
}
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
|
||||
//:: RANKED PAIRS ALGORITHM. :://
|
||||
|
||||
protected function makeResult(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$result = [];
|
||||
$alreadyDone = [];
|
||||
|
||||
$rang = 1;
|
||||
while (\count($alreadyDone) < $election->countCandidates()) {
|
||||
$winners = $this->getWinners($alreadyDone);
|
||||
|
||||
foreach ($this->Arcs as $ArcKey => $Arcvalue) {
|
||||
foreach ($winners as $oneWinner) {
|
||||
if ($Arcvalue['from'] === $oneWinner || $Arcvalue['to'] === $oneWinner) {
|
||||
unset($this->Arcs[$ArcKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result[$rang++] = $winners;
|
||||
array_push($alreadyDone, ...$winners);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getWinners(array $alreadyDone): array
|
||||
{
|
||||
$winners = [];
|
||||
|
||||
foreach (array_keys($this->getElection()->getCandidatesList()) as $candidateKey) {
|
||||
if (!\in_array(needle: $candidateKey, haystack: $alreadyDone, strict: true)) {
|
||||
$win = true;
|
||||
foreach ($this->Arcs as $ArcValue) {
|
||||
if ($ArcValue['to'] === $candidateKey) {
|
||||
$win = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($win) {
|
||||
$winners[] = $candidateKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $winners;
|
||||
}
|
||||
|
||||
|
||||
protected function makeArcs(): void
|
||||
{
|
||||
foreach ($this->PairwiseSort as $newArcsRound) {
|
||||
$virtualArcs = $this->Arcs;
|
||||
$testNewsArcs = [];
|
||||
|
||||
$newKey = max((empty($highKey = array_keys($virtualArcs)) ? [-1] : $highKey)) + 1;
|
||||
foreach ($newArcsRound as $newArc) {
|
||||
$virtualArcs[$newKey] = ['from' => $newArc['from'], 'to' => $newArc['to']];
|
||||
$testNewsArcs[$newKey] = $virtualArcs[$newKey];
|
||||
$newKey++;
|
||||
}
|
||||
|
||||
foreach ($this->getArcsInCycle($virtualArcs) as $cycleArcKey) {
|
||||
if (\array_key_exists($cycleArcKey, $testNewsArcs)) {
|
||||
unset($testNewsArcs[$cycleArcKey]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($testNewsArcs as $newArc) {
|
||||
$this->Arcs[] = $newArc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getArcsInCycle(array $virtualArcs): array
|
||||
{
|
||||
$cycles = [];
|
||||
|
||||
foreach (array_keys($this->getElection()->getCandidatesList()) as $candidateKey) {
|
||||
array_push($cycles, ...$this->followCycle(
|
||||
startCandidateKey: $candidateKey,
|
||||
searchCandidateKey: $candidateKey,
|
||||
virtualArcs: $virtualArcs
|
||||
));
|
||||
}
|
||||
|
||||
return $cycles;
|
||||
}
|
||||
|
||||
protected function followCycle(array $virtualArcs, int $startCandidateKey, int $searchCandidateKey, array &$done = []): array
|
||||
{
|
||||
$arcsInCycle = [];
|
||||
|
||||
foreach ($virtualArcs as $ArcKey => $ArcValue) {
|
||||
if ($ArcValue['from'] === $startCandidateKey) {
|
||||
if (\in_array(needle: $ArcKey, haystack: $done, strict: true)) {
|
||||
continue;
|
||||
} elseif ($ArcValue['to'] === $searchCandidateKey) {
|
||||
$done[] = $ArcKey;
|
||||
$arcsInCycle[] = $ArcKey;
|
||||
} else {
|
||||
$done[] = $ArcKey;
|
||||
array_push(
|
||||
$arcsInCycle,
|
||||
...$this->followCycle(
|
||||
startCandidateKey: $ArcValue['to'],
|
||||
searchCandidateKey: $searchCandidateKey,
|
||||
virtualArcs: $virtualArcs,
|
||||
done: $done
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $arcsInCycle;
|
||||
}
|
||||
|
||||
protected function pairwiseSort(): array
|
||||
{
|
||||
$pairs = [];
|
||||
|
||||
$i = 0;
|
||||
foreach ($this->getElection()->getPairwise() as $candidate_key => $candidate_value) {
|
||||
foreach ($candidate_value['win'] as $challenger_key => $challenger_value) {
|
||||
if ($challenger_value > $candidate_value['lose'][$challenger_key]) {
|
||||
|
||||
// Victory
|
||||
$pairs[$i]['from'] = $candidate_key;
|
||||
// Defeat
|
||||
$pairs[$i]['to'] = $challenger_key;
|
||||
|
||||
$pairs[$i]['win'] = $challenger_value;
|
||||
$pairs[$i]['minority'] = $candidate_value['lose'][$challenger_key];
|
||||
$pairs[$i]['margin'] = $candidate_value['win'][$challenger_key] - $candidate_value['lose'][$challenger_key];
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($pairs, static function (array $a, array $b): int {
|
||||
if ($a[static::RP_VARIANT_1] < $b[static::RP_VARIANT_1]) {
|
||||
return 1;
|
||||
} elseif ($a[static::RP_VARIANT_1] > $b[static::RP_VARIANT_1]) {
|
||||
return -1;
|
||||
} else { // Equal
|
||||
return $a['minority'] <=> $b['minority'];
|
||||
}
|
||||
});
|
||||
|
||||
$newArcs = [];
|
||||
$i = 0;
|
||||
$f = true;
|
||||
foreach (array_keys($pairs) as $pairsKey) {
|
||||
if ($f === true) {
|
||||
$newArcs[$i][] = $pairs[$pairsKey];
|
||||
$f = false;
|
||||
} elseif ($pairs[$pairsKey][static::RP_VARIANT_1] === $pairs[$pairsKey - 1][static::RP_VARIANT_1] && $pairs[$pairsKey]['minority'] === $pairs[$pairsKey - 1]['minority']) {
|
||||
$newArcs[$i][] = $pairs[$pairsKey];
|
||||
} else {
|
||||
$newArcs[++$i][] = $pairs[$pairsKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $newArcs;
|
||||
}
|
||||
}
|
387
include/condorcet/src/Algo/Methods/STV/CPO_STV.php
Normal file
387
include/condorcet/src/Algo/Methods/STV/CPO_STV.php
Normal file
@ -0,0 +1,387 @@
|
||||
<?php
|
||||
/*
|
||||
Part of CPO STV method Module - From the original Condorcet 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\Algo\Methods\STV;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Borda\BordaCount;
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Copeland\Copeland;
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Dodgson\DodgsonTidemanApproximation;
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\InstantRunoff\InstantRunoff;
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Majority\FirstPastThePost;
|
||||
use CondorcetPHP\Condorcet\{Election, Result, Vote};
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, StatsVerbosity};
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\{Combinations, StvQuotas, TieBreakersCollection};
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Minimax\{MinimaxMargin, MinimaxWinning};
|
||||
use CondorcetPHP\Condorcet\Algo\Methods\Schulze\{SchulzeMargin, SchulzeRatio, SchulzeWinning};
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\IntegerOverflowException;
|
||||
use CondorcetPHP\Condorcet\Throwable\MethodLimitReachedException;
|
||||
use SplFixedArray;
|
||||
|
||||
// Single transferable vote | https://en.wikipedia.org/wiki/CPO-STV
|
||||
class CPO_STV extends SingleTransferableVote
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['CPO STV', 'CPO-STV', 'CPO_STV', 'CPO', 'Comparison of Pairs of Outcomes by the Single Transferable Vote', 'Tideman STV'];
|
||||
|
||||
// Limits
|
||||
public static ?int $MaxOutcomeComparisons = 12_000;
|
||||
|
||||
public const DEFAULT_METHODS_CHAINING = [
|
||||
SchulzeMargin::METHOD_NAME[0],
|
||||
SchulzeWinning::METHOD_NAME[0],
|
||||
SchulzeRatio::METHOD_NAME[0],
|
||||
BordaCount::METHOD_NAME[0],
|
||||
Copeland::METHOD_NAME[0],
|
||||
InstantRunoff::METHOD_NAME[0],
|
||||
MinimaxMargin::METHOD_NAME[0],
|
||||
MinimaxWinning::METHOD_NAME[0],
|
||||
DodgsonTidemanApproximation::METHOD_NAME[0],
|
||||
FirstPastThePost::METHOD_NAME[0],
|
||||
];
|
||||
|
||||
public static StvQuotas $optionQuota = StvQuotas::HAGENBACH_BISCHOFF;
|
||||
public static array $optionCondorcetCompletionMethod = self::DEFAULT_METHODS_CHAINING;
|
||||
public static array $optionTieBreakerMethods = self::DEFAULT_METHODS_CHAINING;
|
||||
|
||||
protected ?array $Stats = null;
|
||||
|
||||
protected SplFixedArray $outcomes;
|
||||
protected readonly array $initialScoreTable;
|
||||
protected array $candidatesElectedFromFirstRound = [];
|
||||
protected readonly array $candidatesEliminatedFromFirstRound;
|
||||
protected SplFixedArray $outcomeComparisonTable;
|
||||
protected readonly int $condorcetWinnerOutcome;
|
||||
protected readonly array $completionMethodPairwise;
|
||||
protected readonly Result $completionMethodResult;
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
Vote::initCache(); // Performances
|
||||
$this->outcomes = new SplFixedArray(0);
|
||||
$this->outcomeComparisonTable = new SplFixedArray(0);
|
||||
|
||||
$this->votesNeededToWin = round(self::$optionQuota->getQuota($this->getElection()->sumValidVotesWeightWithConstraints(), $this->getElection()->getNumberOfSeats()), self::DECIMAL_PRECISION, \PHP_ROUND_HALF_DOWN);
|
||||
|
||||
// Compute Initial Score
|
||||
$this->initialScoreTable = $this->makeScore();
|
||||
|
||||
// Candidates elected from first round
|
||||
foreach ($this->initialScoreTable as $candidateKey => $oneScore) {
|
||||
if ($oneScore >= $this->votesNeededToWin) {
|
||||
$this->candidatesElectedFromFirstRound[] = $candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
$numberOfCandidatesNeededToComplete = $this->getElection()->getNumberOfSeats() - \count($this->candidatesElectedFromFirstRound);
|
||||
$this->candidatesEliminatedFromFirstRound = array_diff(array_keys($this->getElection()->getCandidatesList()), $this->candidatesElectedFromFirstRound);
|
||||
|
||||
if ($numberOfCandidatesNeededToComplete > 0 && $numberOfCandidatesNeededToComplete < \count($this->candidatesEliminatedFromFirstRound)) {
|
||||
try {
|
||||
$numberOfComparisons = Combinations::getPossibleCountOfCombinations(
|
||||
count: Combinations::getPossibleCountOfCombinations(
|
||||
count: \count($this->candidatesEliminatedFromFirstRound),
|
||||
length: $numberOfCandidatesNeededToComplete
|
||||
),
|
||||
length: 2
|
||||
);
|
||||
} catch (IntegerOverflowException) {
|
||||
$numberOfComparisons = false;
|
||||
}
|
||||
|
||||
if ($numberOfComparisons === false || (self::$MaxOutcomeComparisons !== null && $numberOfComparisons > self::$MaxOutcomeComparisons)) {
|
||||
throw new MethodLimitReachedException(self::METHOD_NAME[0], self::METHOD_NAME[1].' is currently limited to '.self::$MaxOutcomeComparisons.' comparisons in order to avoid unreasonable deadlocks due to non-polyminial runtime aspects of the algorithm. Consult the manual to increase or remove this limit.');
|
||||
}
|
||||
|
||||
|
||||
// Compute all possible Ranking
|
||||
$this->outcomes = Combinations::compute($this->candidatesEliminatedFromFirstRound, $numberOfCandidatesNeededToComplete, $this->candidatesElectedFromFirstRound);
|
||||
|
||||
// Compare it
|
||||
$this->outcomeComparisonTable->setSize($numberOfComparisons);
|
||||
$this->compareOutcomes();
|
||||
|
||||
// Select the best with a Condorcet method
|
||||
$this->selectBestOutcome();
|
||||
$result = $this->outcomes[$this->condorcetWinnerOutcome];
|
||||
|
||||
// Sort the best Outcome candidate list using originals scores
|
||||
usort($result, function (int $a, int $b): int {
|
||||
return $this->initialScoreTable[$b] <=> $this->initialScoreTable[$a];
|
||||
});
|
||||
} else {
|
||||
$result = array_keys($this->initialScoreTable);
|
||||
|
||||
// Sort the best Outcome candidate list using originals scores, or using others methods
|
||||
$this->sortResultBeforeCut($result);
|
||||
|
||||
// Cut
|
||||
$result = \array_slice($result, 0, $this->getElection()->getNumberOfSeats());
|
||||
}
|
||||
|
||||
// Results: Format Ranks from 1
|
||||
$rank = 0;
|
||||
$lastScore = null;
|
||||
$candidatesDoneCount = 0;
|
||||
$r = [];
|
||||
foreach ($result as $candidateKey) {
|
||||
$score = $this->initialScoreTable[$candidateKey];
|
||||
|
||||
if ($score !== $lastScore) {
|
||||
$rank = $candidatesDoneCount + 1;
|
||||
$lastScore = $score;
|
||||
}
|
||||
|
||||
$r[$rank][] = $candidateKey;
|
||||
$candidatesDoneCount++;
|
||||
}
|
||||
|
||||
// Register result
|
||||
$this->Result = $this->createResult($r);
|
||||
|
||||
Vote::clearCache(); // Performances
|
||||
}
|
||||
|
||||
protected function compareOutcomes(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$index = 0;
|
||||
$key_done = [];
|
||||
|
||||
foreach ($this->outcomes as $MainOutcomeKey => $MainOutcomeR) {
|
||||
foreach ($this->outcomes as $ComparedOutcomeKey => $ComparedOutcomeR) {
|
||||
$outcomeComparisonKey = $this->getOutcomesComparisonKey($MainOutcomeKey, $ComparedOutcomeKey);
|
||||
|
||||
if ($MainOutcomeKey === $ComparedOutcomeKey || \in_array($outcomeComparisonKey, $key_done, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = ['c_key' => $outcomeComparisonKey,
|
||||
'candidates_excluded' => [],
|
||||
];
|
||||
|
||||
// Eliminate Candidates from Outcome
|
||||
foreach (array_keys($election->getCandidatesList()) as $candidateKey) {
|
||||
if (!\in_array($candidateKey, $MainOutcomeR, true) && !\in_array($candidateKey, $ComparedOutcomeR, true)) {
|
||||
$entry['candidates_excluded'][] = $candidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Make score again
|
||||
$entry['scores_after_exclusion'] = $this->makeScore(candidateEliminated: $entry['candidates_excluded']);
|
||||
|
||||
$surplusToTransfer = [];
|
||||
$winnerToJoin = [];
|
||||
foreach ($entry['scores_after_exclusion'] as $candidateKey => $oneScore) {
|
||||
$surplus = $oneScore - $this->votesNeededToWin;
|
||||
|
||||
if ($surplus >= 0 && \in_array($candidateKey, $MainOutcomeR, true) && \in_array($candidateKey, $ComparedOutcomeR, true)) {
|
||||
$surplusToTransfer[$candidateKey] ?? $surplusToTransfer[$candidateKey] = ['surplus' => 0, 'total' => 0];
|
||||
$surplusToTransfer[$candidateKey]['surplus'] += $surplus;
|
||||
$surplusToTransfer[$candidateKey]['total'] += $oneScore;
|
||||
$winnerToJoin[$candidateKey] = $this->votesNeededToWin;
|
||||
}
|
||||
}
|
||||
|
||||
$winnerFromFirstRound = array_keys($winnerToJoin);
|
||||
|
||||
$entry['scores_after_surplus'] = $winnerToJoin + $this->makeScore($surplusToTransfer, $winnerFromFirstRound, $entry['candidates_excluded']);
|
||||
|
||||
// Outcome Score
|
||||
$MainOutcomeScore = 0;
|
||||
$ComparedOutcomeScore = 0;
|
||||
|
||||
foreach ($entry['scores_after_surplus'] as $candidateKey => $candidateScore) {
|
||||
if (\in_array($candidateKey, $MainOutcomeR, true)) {
|
||||
$MainOutcomeScore += $candidateScore;
|
||||
}
|
||||
|
||||
if (\in_array($candidateKey, $ComparedOutcomeR, true)) {
|
||||
$ComparedOutcomeScore += $candidateScore;
|
||||
}
|
||||
}
|
||||
|
||||
$entry['outcomes_scores'] = [$MainOutcomeKey => $MainOutcomeScore, $ComparedOutcomeKey => $ComparedOutcomeScore];
|
||||
|
||||
$key_done[] = $outcomeComparisonKey;
|
||||
$this->outcomeComparisonTable[$index++] = $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getOutcomesComparisonKey(int $MainOutcomeKey, int $ComparedOutcomeKey): string
|
||||
{
|
||||
$minOutcome = (string) min($MainOutcomeKey, $ComparedOutcomeKey);
|
||||
$maxOutcome = (string) max($MainOutcomeKey, $ComparedOutcomeKey);
|
||||
|
||||
return 'Outcome N° '.$minOutcome.' compared to Outcome N° '.$maxOutcome;
|
||||
}
|
||||
|
||||
protected function selectBestOutcome(): void
|
||||
{
|
||||
// With Condorcet
|
||||
$winnerOutcomeElection = new Election;
|
||||
$winnerOutcomeElection->setImplicitRanking(false);
|
||||
$winnerOutcomeElection->allowsVoteWeight(true);
|
||||
$winnerOutcomeElection->setStatsVerbosity($this->getElection()->getStatsVerbosity());
|
||||
|
||||
// Candidates
|
||||
foreach ($this->outcomes as $oneOutcomeKey => $outcomeValue) {
|
||||
$winnerOutcomeElection->addCandidate((string) $oneOutcomeKey);
|
||||
}
|
||||
|
||||
// Votes
|
||||
$coef = Method::DECIMAL_PRECISION ** 10; # Actually, vote weight does not support float
|
||||
foreach ($this->outcomeComparisonTable as $comparison) {
|
||||
($vote1 = new Vote([
|
||||
(string) $key = array_key_first($comparison['outcomes_scores']),
|
||||
(string) array_key_last($comparison['outcomes_scores']),
|
||||
]))->setWeight((int) ($comparison['outcomes_scores'][$key] * $coef));
|
||||
|
||||
($vote2 = new Vote([
|
||||
(string) $key = array_key_last($comparison['outcomes_scores']),
|
||||
(string) array_key_first($comparison['outcomes_scores']),
|
||||
]))->setWeight((int) ($comparison['outcomes_scores'][$key] * $coef));
|
||||
|
||||
$winnerOutcomeElection->addVote($vote1);
|
||||
$winnerOutcomeElection->addVote($vote2);
|
||||
}
|
||||
|
||||
// Selection Winner
|
||||
$selectionSucces = false;
|
||||
|
||||
foreach (self::$optionCondorcetCompletionMethod as $completionMethod) {
|
||||
$completionMethodResult = $winnerOutcomeElection->getResult($completionMethod);
|
||||
$condorcetWinnerOutcome = $completionMethodResult->getWinner();
|
||||
|
||||
if (!\is_array($condorcetWinnerOutcome)) {
|
||||
$selectionSucces = true;
|
||||
$this->completionMethodResult = $completionMethodResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$selectionSucces) {
|
||||
$completionMethodResult = $winnerOutcomeElection->getResult(self::$optionCondorcetCompletionMethod[0]);
|
||||
$condorcetWinnerOutcome = $completionMethodResult->getWinner();
|
||||
$condorcetWinnerOutcome = reset($condorcetWinnerOutcome);
|
||||
$this->completionMethodResult = $completionMethodResult;
|
||||
}
|
||||
|
||||
$this->condorcetWinnerOutcome = (int) $condorcetWinnerOutcome->getName();
|
||||
$this->completionMethodPairwise = $winnerOutcomeElection->getExplicitPairwise();
|
||||
}
|
||||
|
||||
protected function sortResultBeforeCut(array &$result): void
|
||||
{
|
||||
usort($result, function (int $a, int $b): int {
|
||||
$tieBreakerFromInitialScore = $this->initialScoreTable[$b] <=> $this->initialScoreTable[$a];
|
||||
|
||||
if ($tieBreakerFromInitialScore !== 0) {
|
||||
return $tieBreakerFromInitialScore;
|
||||
} else {
|
||||
$election = $this->getElection();
|
||||
|
||||
if (\count($tiebreaker = TieBreakersCollection::tieBreakerWithAnotherMethods($election, self::$optionTieBreakerMethods, [$a, $b])) === 1) {
|
||||
$w = reset($tiebreaker);
|
||||
return ($w === $a) ? -1 : 1;
|
||||
} else {
|
||||
return mb_strtolower($election->getCandidateObjectFromKey($b)->getName(), 'UTF-8') <=> mb_strtolower($election->getCandidateObjectFromKey($b)->getName(), 'UTF-8');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Stats
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$stats = ['Votes Needed to Win' => $this->votesNeededToWin];
|
||||
|
||||
$changeKeyToCandidateAndSortByName = static function (array $arr, Election $election): array {
|
||||
$r = [];
|
||||
foreach ($arr as $candidateKey => $value) {
|
||||
$r[(string) $election->getCandidateObjectFromKey($candidateKey)] = $value;
|
||||
}
|
||||
|
||||
ksort($r, \SORT_NATURAL);
|
||||
return $r;
|
||||
};
|
||||
|
||||
$changeValueToCandidateAndSortByName = static function (array $arr, Election $election): array {
|
||||
$r = [];
|
||||
foreach ($arr as $candidateKey) {
|
||||
$r[] = (string) $election->getCandidateObjectFromKey($candidateKey);
|
||||
}
|
||||
|
||||
sort($r, \SORT_NATURAL);
|
||||
return $r;
|
||||
};
|
||||
|
||||
// Stats >= STD
|
||||
if ($election->getStatsVerbosity()->value >= StatsVerbosity::STD->value) {
|
||||
// Initial Scores Table
|
||||
$stats['Initial Score Table'] = $changeKeyToCandidateAndSortByName($this->initialScoreTable, $election);
|
||||
|
||||
// Candidates Elected from first round
|
||||
$stats['Candidates elected from first round'] = $changeValueToCandidateAndSortByName($this->candidatesElectedFromFirstRound, $election);
|
||||
|
||||
// Candidates Eliminated from first round
|
||||
$stats['Candidates eliminated from first round'] = $changeValueToCandidateAndSortByName($this->candidatesEliminatedFromFirstRound, $election);
|
||||
|
||||
// Completion Method
|
||||
if (isset($this->completionMethodResult)) {
|
||||
$stats['Completion Method'] = $this->completionMethodResult->fromMethod;
|
||||
}
|
||||
}
|
||||
|
||||
// Stats >= HIGH
|
||||
if ($election->getStatsVerbosity()->value >= StatsVerbosity::HIGH->value) {
|
||||
// Completion method Stats
|
||||
if (isset($this->completionMethodResult)) {
|
||||
$stats['Condorcet Completion Method Stats'] = [
|
||||
'Pairwise' => $this->completionMethodPairwise,
|
||||
'Stats' => $this->completionMethodResult->getStats(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Stats >= FULL
|
||||
if ($election->getStatsVerbosity()->value >= StatsVerbosity::FULL->value) {
|
||||
// Outcome
|
||||
foreach ($this->outcomes as $outcomeKey => $outcomeValue) {
|
||||
$stats['Outcomes'][$outcomeKey] = $changeValueToCandidateAndSortByName($outcomeValue, $election);
|
||||
}
|
||||
|
||||
// Outcomes Comparison
|
||||
foreach ($this->outcomeComparisonTable as $octValue) {
|
||||
foreach ($octValue as $octDetailsKey => $octDetailsValue) {
|
||||
if ($octDetailsKey === 'candidates_excluded') {
|
||||
$stats['Outcomes Comparison'][$octValue['c_key']][$octDetailsKey] = $changeValueToCandidateAndSortByName($octDetailsValue, $election);
|
||||
} elseif ($octDetailsKey === 'outcomes_scores') {
|
||||
$stats['Outcomes Comparison'][$octValue['c_key']][$octDetailsKey] = $octDetailsValue;
|
||||
} elseif (\is_array($octDetailsValue)) {
|
||||
$stats['Outcomes Comparison'][$octValue['c_key']][$octDetailsKey] = $changeKeyToCandidateAndSortByName($octDetailsValue, $election);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return
|
||||
return $stats;
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/*
|
||||
Part of STV method Module - From the original Condorcet 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\Algo\Methods\STV;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface, StatsVerbosity};
|
||||
use CondorcetPHP\Condorcet\Algo\Tools\StvQuotas;
|
||||
use CondorcetPHP\Condorcet\Vote;
|
||||
|
||||
// Single transferable vote | https://en.wikipedia.org/wiki/Single_transferable_vote
|
||||
class SingleTransferableVote extends Method implements MethodInterface
|
||||
{
|
||||
final public const IS_PROPORTIONAL = true;
|
||||
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['STV', 'Single Transferable Vote', 'SingleTransferableVote'];
|
||||
|
||||
public static StvQuotas $optionQuota = StvQuotas::DROOP;
|
||||
|
||||
protected ?array $Stats = null;
|
||||
|
||||
protected float $votesNeededToWin;
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
protected function compute(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
Vote::initCache(); // Performances
|
||||
|
||||
$result = [];
|
||||
$rank = 0;
|
||||
|
||||
$this->votesNeededToWin = round(self::$optionQuota->getQuota($election->sumValidVotesWeightWithConstraints(), $election->getNumberOfSeats()), self::DECIMAL_PRECISION, \PHP_ROUND_HALF_DOWN);
|
||||
|
||||
$candidateElected = [];
|
||||
$candidateEliminated = [];
|
||||
|
||||
$end = false;
|
||||
$round = 0;
|
||||
|
||||
$surplusToTransfer = [];
|
||||
|
||||
while (!$end) {
|
||||
$scoreTable = $this->makeScore($surplusToTransfer, $candidateElected, $candidateEliminated);
|
||||
ksort($scoreTable, \SORT_NATURAL);
|
||||
arsort($scoreTable, \SORT_NUMERIC);
|
||||
|
||||
$successOnRank = false;
|
||||
|
||||
foreach ($scoreTable as $candidateKey => $oneScore) {
|
||||
$surplus = $oneScore - $this->votesNeededToWin;
|
||||
|
||||
if ($surplus >= 0) {
|
||||
$result[++$rank] = [$candidateKey];
|
||||
$candidateElected[] = $candidateKey;
|
||||
|
||||
$surplusToTransfer[$candidateKey] ?? $surplusToTransfer[$candidateKey] = ['surplus' => 0, 'total' => 0];
|
||||
$surplusToTransfer[$candidateKey]['surplus'] += $surplus;
|
||||
$surplusToTransfer[$candidateKey]['total'] += $oneScore;
|
||||
$successOnRank = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$successOnRank && !empty($scoreTable)) {
|
||||
$candidateEliminated[] = array_key_last($scoreTable);
|
||||
} elseif (empty($scoreTable) || $rank >= $election->getNumberOfSeats()) {
|
||||
$end = true;
|
||||
}
|
||||
|
||||
$this->Stats[++$round] = $scoreTable;
|
||||
}
|
||||
|
||||
while ($rank < $election->getNumberOfSeats() && !empty($candidateEliminated)) {
|
||||
$rescueCandidateKey = array_key_last($candidateEliminated);
|
||||
$result[++$rank] = $candidateEliminated[$rescueCandidateKey];
|
||||
unset($candidateEliminated[$rescueCandidateKey]);
|
||||
}
|
||||
|
||||
$this->Result = $this->createResult($result);
|
||||
|
||||
Vote::clearCache(); // Performances
|
||||
}
|
||||
|
||||
protected function makeScore(array $surplus = [], array $candidateElected = [], array $candidateEliminated = []): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$scoreTable = [];
|
||||
|
||||
$candidateDone = array_merge($candidateElected, $candidateEliminated);
|
||||
|
||||
foreach (array_keys($election->getCandidatesList()) as $oneCandidateKey) {
|
||||
if (!\in_array($candidateKey = $oneCandidateKey, $candidateDone, true)) {
|
||||
$scoreTable[$candidateKey] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($election->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
$weight = $oneVote->getWeight($election);
|
||||
|
||||
$winnerBonusWeight = 0;
|
||||
$winnerBonusKey = null;
|
||||
$LoserBonusWeight = 0;
|
||||
|
||||
$firstRank = true;
|
||||
foreach ($oneVote->getContextualRankingWithoutSort($election) as $oneRank) {
|
||||
foreach ($oneRank as $oneCandidate) {
|
||||
if (\count($oneRank) !== 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
$candidateKey = $election->getCandidateKey($oneCandidate);
|
||||
|
||||
if ($firstRank) {
|
||||
if (\array_key_exists($candidateKey, $surplus)) {
|
||||
$winnerBonusWeight = $weight;
|
||||
$winnerBonusKey = $candidateKey;
|
||||
$firstRank = false;
|
||||
break;
|
||||
} elseif (\in_array($candidateKey, $candidateEliminated, true)) {
|
||||
$LoserBonusWeight = $weight;
|
||||
$firstRank = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists($candidateKey, $scoreTable)) {
|
||||
if ($winnerBonusKey !== null) {
|
||||
$scoreTable[$candidateKey] += $winnerBonusWeight / $surplus[$winnerBonusKey]['total'] * $surplus[$winnerBonusKey]['surplus'];
|
||||
} elseif ($LoserBonusWeight > 0) {
|
||||
$scoreTable[$candidateKey] += $LoserBonusWeight;
|
||||
} else {
|
||||
$scoreTable[$candidateKey] += $weight;
|
||||
}
|
||||
|
||||
$scoreTable[$candidateKey] = round($scoreTable[$candidateKey], self::DECIMAL_PRECISION, \PHP_ROUND_HALF_DOWN);
|
||||
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $scoreTable;
|
||||
}
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$stats = ['Votes Needed to Win' => $this->votesNeededToWin];
|
||||
|
||||
if ($election->getStatsVerbosity()->value > StatsVerbosity::LOW->value) {
|
||||
$stats['rounds'] = [];
|
||||
|
||||
foreach ($this->Stats as $roundNumber => $roundData) {
|
||||
foreach ($roundData as $candidateKey => $candidateValue) {
|
||||
$stats['rounds'][$roundNumber][(string) $election->getCandidateObjectFromKey($candidateKey)] = $candidateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
27
include/condorcet/src/Algo/Methods/Schulze/SchulzeMargin.php
Normal file
27
include/condorcet/src/Algo/Methods/Schulze/SchulzeMargin.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/*
|
||||
Part of SCHULZE method Module - From the original Condorcet 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\Algo\Methods\Schulze;
|
||||
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class SchulzeMargin extends Schulze_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Schulze Margin', 'SchulzeMargin', 'Schulze_Margin'];
|
||||
|
||||
protected function schulzeVariant(int $i, int $j, Election $election): int
|
||||
{
|
||||
return $election->getPairwise()[$i]['win'][$j] - $election->getPairwise()[$j]['win'][$i];
|
||||
}
|
||||
}
|
31
include/condorcet/src/Algo/Methods/Schulze/SchulzeRatio.php
Normal file
31
include/condorcet/src/Algo/Methods/Schulze/SchulzeRatio.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*
|
||||
Part of SCHULZE method Module - From the original Condorcet 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\Algo\Methods\Schulze;
|
||||
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class SchulzeRatio extends Schulze_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Schulze Ratio', 'SchulzeRatio', 'Schulze_Ratio'];
|
||||
|
||||
protected function schulzeVariant(int $i, int $j, Election $election): float
|
||||
{
|
||||
if ($election->getPairwise()[$j]['win'][$i] !== 0) {
|
||||
return (float) ($election->getPairwise()[$i]['win'][$j] / $election->getPairwise()[$j]['win'][$i]);
|
||||
} else {
|
||||
return (float) (($election->getPairwise()[$i]['win'][$j] +1) / 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/*
|
||||
Part of SCHULZE method Module - From the original Condorcet 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\Algo\Methods\Schulze;
|
||||
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
class SchulzeWinning extends Schulze_Core
|
||||
{
|
||||
// Method Name
|
||||
public const METHOD_NAME = ['Schulze Winning', 'Schulze', 'SchulzeWinning', 'Schulze_Winning', 'Schwartz Sequential Dropping', 'SSD', 'Cloneproof Schwartz Sequential Dropping', 'CSSD', 'Beatpath', 'Beatpath Method', 'Beatpath Winner', 'Path Voting', 'Path Winner'];
|
||||
|
||||
protected function schulzeVariant(int $i, int $j, Election $election): int
|
||||
{
|
||||
return $election->getPairwise()[$i]['win'][$j];
|
||||
}
|
||||
}
|
178
include/condorcet/src/Algo/Methods/Schulze/Schulze_Core.php
Normal file
178
include/condorcet/src/Algo/Methods/Schulze/Schulze_Core.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/*
|
||||
Part of SCHULZE method Module - From the original Condorcet 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\Algo\Methods\Schulze;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Election, Result};
|
||||
use CondorcetPHP\Condorcet\Algo\{Method, MethodInterface};
|
||||
|
||||
// Schulze is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Schulze_method
|
||||
abstract class Schulze_Core extends Method implements MethodInterface
|
||||
{
|
||||
// Schulze
|
||||
protected array $StrongestPaths = [];
|
||||
|
||||
|
||||
/////////// PUBLIC ///////////
|
||||
|
||||
abstract protected function schulzeVariant(int $i, int $j, Election $election): int|float;
|
||||
|
||||
public function getResult(): Result
|
||||
{
|
||||
// Cache
|
||||
if ($this->Result !== null) {
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// Format array
|
||||
$this->prepareStrongestPath();
|
||||
|
||||
// Strongest Paths calculation
|
||||
$this->makeStrongestPaths();
|
||||
|
||||
// Ranking calculation
|
||||
$this->makeRanking();
|
||||
|
||||
|
||||
// Return
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
|
||||
// Get the Schulze ranking
|
||||
protected function getStats(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$explicit = [];
|
||||
|
||||
foreach ($this->StrongestPaths as $candidate_key => $candidate_value) {
|
||||
$candidate_key = $election->getCandidateObjectFromKey($candidate_key)->getName();
|
||||
|
||||
foreach ($candidate_value as $challenger_key => $challenger_value) {
|
||||
$explicit[$candidate_key][$election->getCandidateObjectFromKey($challenger_key)->getName()] = $challenger_value;
|
||||
}
|
||||
}
|
||||
|
||||
return $explicit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////// COMPUTE ///////////
|
||||
|
||||
|
||||
//:: SCHULZE ALGORITHM. :://
|
||||
|
||||
|
||||
// Calculate the strongest Paths for Schulze Method
|
||||
protected function prepareStrongestPath(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$CandidatesKeys = array_keys($election->getCandidatesList());
|
||||
|
||||
foreach ($CandidatesKeys as $candidate_key) {
|
||||
$this->StrongestPaths[$candidate_key] = [];
|
||||
|
||||
// Format array for the strongest path
|
||||
foreach ($CandidatesKeys as $candidate_key_r) {
|
||||
if ($candidate_key_r !== $candidate_key) {
|
||||
$this->StrongestPaths[$candidate_key][$candidate_key_r] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate the Strongest Paths
|
||||
protected function makeStrongestPaths(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$CandidatesKeys = array_keys($election->getCandidatesList());
|
||||
|
||||
foreach ($CandidatesKeys as $i) {
|
||||
foreach ($CandidatesKeys as $j) {
|
||||
if ($i !== $j) {
|
||||
if ($election->getPairwise()[$i]['win'][$j] > $election->getPairwise()[$j]['win'][$i]) {
|
||||
$this->StrongestPaths[$i][$j] = $this->schulzeVariant($i, $j, $election);
|
||||
} else {
|
||||
$this->StrongestPaths[$i][$j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($CandidatesKeys as $i) {
|
||||
foreach ($CandidatesKeys as $j) {
|
||||
if ($i !== $j) {
|
||||
foreach ($CandidatesKeys as $k) {
|
||||
if ($i !== $k && $j !== $k) {
|
||||
$this->StrongestPaths[$j][$k] =
|
||||
max(
|
||||
$this->StrongestPaths[$j][$k],
|
||||
min($this->StrongestPaths[$j][$i], $this->StrongestPaths[$i][$k])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate && Format human readable ranking
|
||||
protected function makeRanking(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$result = [];
|
||||
|
||||
// Calculate ranking
|
||||
$done = [];
|
||||
$rank = 1;
|
||||
|
||||
while (\count($done) < $election->countCandidates()) {
|
||||
$to_done = [];
|
||||
|
||||
foreach ($this->StrongestPaths as $candidate_key => $challengers_key) {
|
||||
if (\in_array(needle: $candidate_key, haystack: $done, strict: true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$winner = true;
|
||||
|
||||
foreach ($challengers_key as $beaten_key => $beaten_value) {
|
||||
if (\in_array(needle: $beaten_key, haystack: $done, strict: true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($beaten_value < $this->StrongestPaths[$beaten_key][$candidate_key]) {
|
||||
$winner = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($winner) {
|
||||
$result[$rank][] = $candidate_key;
|
||||
|
||||
$to_done[] = $candidate_key;
|
||||
}
|
||||
}
|
||||
|
||||
array_push($done, ...$to_done);
|
||||
|
||||
$rank++;
|
||||
}
|
||||
|
||||
$this->Result = $this->createResult($result);
|
||||
}
|
||||
}
|
232
include/condorcet/src/Algo/Pairwise.php
Normal file
232
include/condorcet/src/Algo/Pairwise.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?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\Algo;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Condorcet, CondorcetVersion, Election, Vote};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionReturn, PublicAPI, Related};
|
||||
use CondorcetPHP\Condorcet\Timer\Chrono as Timer_Chrono;
|
||||
|
||||
class Pairwise implements \ArrayAccess, \Iterator
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
// Implement ArrayAccess
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->Pairwise[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): ?array
|
||||
{
|
||||
return $this->Pairwise[$offset] ?? null;
|
||||
}
|
||||
|
||||
|
||||
// Implement Iterator
|
||||
private bool $valid = true;
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
reset($this->Pairwise);
|
||||
$this->valid = true;
|
||||
}
|
||||
|
||||
public function current(): array
|
||||
{
|
||||
return $this->Pairwise[$this->key()];
|
||||
}
|
||||
|
||||
public function key(): ?int
|
||||
{
|
||||
return key($this->Pairwise);
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
if (next($this->Pairwise) === false) {
|
||||
$this->valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->valid;
|
||||
}
|
||||
|
||||
|
||||
// Pairwise
|
||||
|
||||
protected \WeakReference $Election;
|
||||
protected array $Pairwise_Model;
|
||||
protected array $Pairwise;
|
||||
|
||||
public function __construct(Election $link)
|
||||
{
|
||||
$this->setElection($link);
|
||||
$this->formatNewpairwise();
|
||||
$this->doPairwise();
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'Pairwise_Model' => $this->Pairwise_Model,
|
||||
'Pairwise' => $this->Pairwise,
|
||||
];
|
||||
}
|
||||
|
||||
public function getElection(): Election
|
||||
{
|
||||
return $this->Election->get();
|
||||
}
|
||||
|
||||
public function setElection(Election $election): void
|
||||
{
|
||||
$this->Election = \WeakReference::create($election);
|
||||
}
|
||||
|
||||
public function addNewVote(int $key): void
|
||||
{
|
||||
(Condorcet::$UseTimer === true) && new Timer_Chrono($this->getElection()->getTimerManager(), 'Add Vote To Pairwise');
|
||||
|
||||
$this->computeOneVote($this->Pairwise, $this->getElection()->getVotesManager()[$key]);
|
||||
}
|
||||
|
||||
public function removeVote(int $key): void
|
||||
{
|
||||
(Condorcet::$UseTimer === true) && new Timer_Chrono($this->getElection()->getTimerManager(), 'Remove Vote To Pairwise');
|
||||
|
||||
$diff = $this->Pairwise_Model;
|
||||
|
||||
$this->computeOneVote($diff, $this->getElection()->getVotesManager()[$key]);
|
||||
|
||||
foreach ($diff as $candidate_key => $candidate_details) {
|
||||
foreach ($candidate_details as $type => $opponent) {
|
||||
foreach ($opponent as $opponent_key => $score) {
|
||||
$this->Pairwise[$candidate_key][$type][$opponent_key] -= $score;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the Pairwise.')]
|
||||
#[FunctionReturn('Pairwise as an explicit array .')]
|
||||
#[Related('Election::getPairwise', 'Election::getResult')]
|
||||
public function getExplicitPairwise(): array
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$explicit_pairwise = [];
|
||||
|
||||
foreach ($this->Pairwise as $candidate_key => $candidate_value) {
|
||||
$candidate_name = $election->getCandidateObjectFromKey($candidate_key)->getName();
|
||||
|
||||
foreach ($candidate_value as $mode => $mode_value) {
|
||||
foreach ($mode_value as $candidate_list_key => $candidate_list_value) {
|
||||
$explicit_pairwise[$candidate_name][$mode][$election->getCandidateObjectFromKey($candidate_list_key)->getName()] = $candidate_list_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $explicit_pairwise;
|
||||
}
|
||||
|
||||
protected function formatNewpairwise(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$this->Pairwise_Model = [];
|
||||
|
||||
foreach ($election->getCandidatesList() as $candidate_key => $candidate_id) {
|
||||
$this->Pairwise_Model[$candidate_key] = ['win' => [], 'null' => [], 'lose' => []];
|
||||
|
||||
foreach ($election->getCandidatesList() as $candidate_key_r => $candidate_id_r) {
|
||||
if ($candidate_key_r !== $candidate_key) {
|
||||
$this->Pairwise_Model[$candidate_key]['win'][$candidate_key_r] = 0;
|
||||
$this->Pairwise_Model[$candidate_key]['null'][$candidate_key_r] = 0;
|
||||
$this->Pairwise_Model[$candidate_key]['lose'][$candidate_key_r] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function doPairwise(): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
// Chrono
|
||||
(Condorcet::$UseTimer === true) && new Timer_Chrono($election->getTimerManager(), 'Do Pairwise');
|
||||
|
||||
$this->Pairwise = $this->Pairwise_Model;
|
||||
|
||||
foreach ($election->getVotesManager()->getVotesValidUnderConstraintGenerator() as $oneVote) {
|
||||
$this->computeOneVote($this->Pairwise, $oneVote);
|
||||
}
|
||||
}
|
||||
|
||||
protected function computeOneVote(array &$pairwise, Vote $oneVote): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$vote_ranking = $oneVote->getContextualRankingWithoutSort($election);
|
||||
$voteWeight = $oneVote->getWeight($election);
|
||||
|
||||
$vote_candidate_list = [];
|
||||
|
||||
foreach ($vote_ranking as $rank) {
|
||||
foreach ($rank as $oneCandidate) {
|
||||
$vote_candidate_list[] = $election->getCandidateKey($oneCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
$done_Candidates = [];
|
||||
|
||||
foreach ($vote_ranking as $candidates_in_rank) {
|
||||
$candidates_in_rank_keys = [];
|
||||
|
||||
foreach ($candidates_in_rank as $candidate) {
|
||||
$candidates_in_rank_keys[] = $election->getCandidateKey($candidate);
|
||||
}
|
||||
|
||||
foreach ($candidates_in_rank as $candidate) {
|
||||
$candidate_key = $election->getCandidateKey($candidate);
|
||||
|
||||
// Process
|
||||
foreach ($vote_candidate_list as $opponent_candidate_key) {
|
||||
if ($candidate_key !== $opponent_candidate_key) {
|
||||
$opponent_in_rank = null;
|
||||
|
||||
// Win & Lose
|
||||
if (!\in_array(needle: $opponent_candidate_key, haystack: $done_Candidates, strict: true) &&
|
||||
!($opponent_in_rank = \in_array(needle: $opponent_candidate_key, haystack: $candidates_in_rank_keys, strict: true))) {
|
||||
$pairwise[$candidate_key]['win'][$opponent_candidate_key] += $voteWeight;
|
||||
$pairwise[$opponent_candidate_key]['lose'][$candidate_key] += $voteWeight;
|
||||
|
||||
// Null
|
||||
} elseif ($opponent_in_rank ?? \in_array(needle: $opponent_candidate_key, haystack: $candidates_in_rank_keys, strict: true)) {
|
||||
$pairwise[$candidate_key]['null'][$opponent_candidate_key] += $voteWeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$done_Candidates[] = $candidate_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
include/condorcet/src/Algo/StatsVerbosity.php
Normal file
23
include/condorcet/src/Algo/StatsVerbosity.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?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\Algo;
|
||||
|
||||
// Manage Candidates for Election class
|
||||
enum StatsVerbosity: int
|
||||
{
|
||||
case NONE = 0; // No stats
|
||||
case LOW = 100; // Minal Stats
|
||||
case STD = 200; // Standards Stats
|
||||
case HIGH = 300; // High Level of stats
|
||||
case FULL = 400; // Fulls stats
|
||||
case DEBUG = 500; // More full
|
||||
}
|
103
include/condorcet/src/Algo/Tools/Combinations.php
Normal file
103
include/condorcet/src/Algo/Tools/Combinations.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?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\Algo\Tools;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
use Brick\Math\Exception\IntegerOverflowException;
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\{CondorcetInternalException, IntegerOverflowException as CondorcetIntegerOverflowException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{InternalModulesAPI, PublicAPI};
|
||||
use SplFixedArray;
|
||||
|
||||
#[InternalModulesAPI]
|
||||
class Combinations
|
||||
{
|
||||
#[PublicAPI] // Must be available with composer installation. Only appliez to getPossibleCountOfCombinations() method. PHP and memory can't do the compute() with such large numbers.
|
||||
public static bool $useBigIntegerIfAvailable = true;
|
||||
|
||||
public static function getPossibleCountOfCombinations(int $count, int $length): int
|
||||
{
|
||||
if ($count < 1 || $length < 1 || $count < $length) {
|
||||
throw new CondorcetInternalException('Parameters invalid');
|
||||
}
|
||||
|
||||
if (self::$useBigIntegerIfAvailable && class_exists('Brick\Math\BigInteger')) {
|
||||
$a = BigInteger::of(1);
|
||||
for ($i = $count; $i > ($count - $length); $i--) {
|
||||
$a = $a->multipliedBy($i);
|
||||
}
|
||||
|
||||
$b = BigInteger::of(1);
|
||||
for ($i = $length; $i > 0; $i--) {
|
||||
$b = $b->multipliedBy($i);
|
||||
}
|
||||
|
||||
$r = $a->dividedBy($b);
|
||||
|
||||
try {
|
||||
return $r->toInt();
|
||||
} catch (IntegerOverflowException $e) {
|
||||
throw new CondorcetIntegerOverflowException($e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$a = 1;
|
||||
for ($i = $count; $i > ($count - $length); $i--) {
|
||||
$a *= $i;
|
||||
}
|
||||
|
||||
$b = 1;
|
||||
for ($i = $length; $i > 0; $i--) {
|
||||
$b *= $i;
|
||||
}
|
||||
|
||||
if (\is_float($a) || \is_float($b)) { // @phpstan-ignore-line
|
||||
throw new CondorcetIntegerOverflowException;
|
||||
} else {
|
||||
return (int) ($a / $b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function compute(array $values, int $length, array $append_before = []): SplFixedArray
|
||||
{
|
||||
$count = \count($values);
|
||||
$r = new SplFixedArray(self::getPossibleCountOfCombinations($count, $length));
|
||||
|
||||
$arrKey = 0;
|
||||
foreach (self::computeGenerator($values, $length, $append_before) as $oneCombination) {
|
||||
$r[$arrKey++] = $oneCombination;
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
public static function computeGenerator(array $values, int $length, array $append_before = []): \Generator
|
||||
{
|
||||
$count = \count($values);
|
||||
$size = 2 ** $count;
|
||||
$keys = array_keys($values);
|
||||
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$b = sprintf('%0' . $count . 'b', $i);
|
||||
$out = [];
|
||||
|
||||
for ($j = 0; $j < $count; $j++) {
|
||||
if ($b[$j] === '1') {
|
||||
$out[$keys[$j]] = $values[$keys[$j]];
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($out) === $length) {
|
||||
yield array_values(array_merge($append_before, $out));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
include/condorcet/src/Algo/Tools/PairwiseStats.php
Normal file
71
include/condorcet/src/Algo/Tools/PairwiseStats.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?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);
|
||||
|
||||
/////////// TOOLS FOR MODULAR ALGORITHMS ///////////
|
||||
|
||||
namespace CondorcetPHP\Condorcet\Algo\Tools;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\Pairwise;
|
||||
|
||||
// Generic for Algorithms
|
||||
abstract class PairwiseStats
|
||||
{
|
||||
public static function PairwiseComparison(Pairwise $pairwise): array
|
||||
{
|
||||
$comparison = [];
|
||||
|
||||
foreach ($pairwise as $candidate_key => $candidate_data) {
|
||||
$comparison[$candidate_key]['win'] = 0;
|
||||
$comparison[$candidate_key]['null'] = 0;
|
||||
$comparison[$candidate_key]['lose'] = 0;
|
||||
$comparison[$candidate_key]['balance'] = 0;
|
||||
$comparison[$candidate_key]['sum_defeat_margin'] = 0;
|
||||
$comparison[$candidate_key]['worst_pairwise_defeat_winning'] = 0;
|
||||
$comparison[$candidate_key]['worst_pairwise_defeat_margin'] = null;
|
||||
$comparison[$candidate_key]['worst_pairwise_opposition'] = 0;
|
||||
|
||||
foreach ($candidate_data['win'] as $opponentKey => $opponentLose) {
|
||||
$defeat_margin = $candidate_data['lose'][$opponentKey] - $opponentLose;
|
||||
|
||||
// Worst margin defeat
|
||||
if ($comparison[$candidate_key]['worst_pairwise_defeat_margin'] === null || $comparison[$candidate_key]['worst_pairwise_defeat_margin'] < $defeat_margin) {
|
||||
$comparison[$candidate_key]['worst_pairwise_defeat_margin'] = $defeat_margin;
|
||||
}
|
||||
|
||||
// Worst pairwise opposition
|
||||
if ($comparison[$candidate_key]['worst_pairwise_opposition'] < $candidate_data['lose'][$opponentKey]) {
|
||||
$comparison[$candidate_key]['worst_pairwise_opposition'] = $candidate_data['lose'][$opponentKey];
|
||||
}
|
||||
|
||||
|
||||
// for each Win, null, Lose
|
||||
if ($opponentLose > $candidate_data['lose'][$opponentKey]) {
|
||||
$comparison[$candidate_key]['win']++;
|
||||
$comparison[$candidate_key]['balance']++;
|
||||
} elseif ($opponentLose === $candidate_data['lose'][$opponentKey]) {
|
||||
$comparison[$candidate_key]['null']++;
|
||||
} else {
|
||||
$comparison[$candidate_key]['lose']++;
|
||||
$comparison[$candidate_key]['balance']--;
|
||||
|
||||
$comparison[$candidate_key]['sum_defeat_margin'] += $defeat_margin;
|
||||
|
||||
// Worst winning defeat
|
||||
if ($comparison[$candidate_key]['worst_pairwise_defeat_winning'] < $candidate_data['lose'][$opponentKey]) {
|
||||
$comparison[$candidate_key]['worst_pairwise_defeat_winning'] = $candidate_data['lose'][$opponentKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $comparison;
|
||||
}
|
||||
}
|
106
include/condorcet/src/Algo/Tools/Permutations.php
Normal file
106
include/condorcet/src/Algo/Tools/Permutations.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?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\Algo\Tools;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
use Brick\Math\Exception\IntegerOverflowException;
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\{CondorcetInternalException, IntegerOverflowException as CondorcetIntegerOverflowException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{InternalModulesAPI, PublicAPI};
|
||||
use SplFixedArray;
|
||||
|
||||
// Thanks to Jorge Gomes @cyberkurumin
|
||||
#[InternalModulesAPI]
|
||||
class Permutations
|
||||
{
|
||||
#[PublicAPI] // Must be available with composer installation. Only applied to getPossibleCountOfPermutations() method. PHP and memory can't do the compute() with such large numbers.
|
||||
public static bool $useBigIntegerIfAvailable = true;
|
||||
|
||||
protected readonly array $candidates;
|
||||
|
||||
public static function getPossibleCountOfPermutations(int $candidatesNumber): int
|
||||
{
|
||||
if ($candidatesNumber < 1) {
|
||||
throw new CondorcetInternalException('Parameters invalid');
|
||||
}
|
||||
|
||||
if (self::$useBigIntegerIfAvailable && class_exists('Brick\Math\BigInteger')) {
|
||||
$result = BigInteger::of($candidatesNumber);
|
||||
|
||||
for ($iteration = 1; $iteration < $candidatesNumber; $iteration++) {
|
||||
$result = $result->multipliedBy($candidatesNumber - $iteration);
|
||||
}
|
||||
|
||||
try {
|
||||
return $result->toInt();
|
||||
} catch (IntegerOverflowException $e) {
|
||||
throw new CondorcetIntegerOverflowException($e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$result = $candidatesNumber;
|
||||
|
||||
for ($iteration = 1; $iteration < $candidatesNumber; $iteration++) {
|
||||
$result = $result * ($candidatesNumber - $iteration);
|
||||
}
|
||||
|
||||
if (\is_float($result)) { // @phpstan-ignore-line
|
||||
throw new CondorcetIntegerOverflowException;
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(array $candidates)
|
||||
{
|
||||
$this->candidates = array_values($candidates);
|
||||
}
|
||||
|
||||
public function getResults(): SplFixedArray
|
||||
{
|
||||
$results = new SplFixedArray(self::getPossibleCountOfPermutations(\count($this->candidates)));
|
||||
$arrKey = 0;
|
||||
|
||||
foreach ($this->getPermutationGenerator() as $onePermutation) {
|
||||
$results[$arrKey++] = $onePermutation;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getPermutationGenerator(): \Generator
|
||||
{
|
||||
return $this->permutationGenerator($this->candidates);
|
||||
}
|
||||
|
||||
protected function permutationGenerator(array $elements): \Generator
|
||||
{
|
||||
if (\count($elements) <= 1) {
|
||||
yield [1 => reset($elements)]; // Set the only key to index 1
|
||||
} else {
|
||||
foreach ($this->permutationGenerator(\array_slice($elements, 1)) as $permutation) {
|
||||
foreach (range(0, \count($elements) - 1) as $i) {
|
||||
$r = array_merge(
|
||||
\array_slice($permutation, 0, $i),
|
||||
[$elements[0]],
|
||||
\array_slice($permutation, $i)
|
||||
);
|
||||
|
||||
// Set first key to 1
|
||||
$r = [null, ...$r];
|
||||
unset($r[0]);
|
||||
|
||||
yield $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
include/condorcet/src/Algo/Tools/StvQuotas.php
Normal file
57
include/condorcet/src/Algo/Tools/StvQuotas.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?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);
|
||||
|
||||
/////////// TOOLS FOR MODULAR ALGORITHMS ///////////
|
||||
|
||||
namespace CondorcetPHP\Condorcet\Algo\Tools;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, PublicAPI};
|
||||
use CondorcetPHP\Condorcet\Throwable\StvQuotaNotImplementedException;
|
||||
|
||||
// Generic for Algorithms
|
||||
#[PublicAPI]
|
||||
enum StvQuotas: string
|
||||
{
|
||||
case DROOP = 'Droop Quota';
|
||||
case HARE = 'Hare Quota';
|
||||
case HAGENBACH_BISCHOFF = 'Hagenbach-Bischoff Quota';
|
||||
case IMPERIALI = 'Imperiali Quota';
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Build the Enum Quotas option for STV methods')]
|
||||
#[FunctionReturn('The Quota option')]
|
||||
#[Example('Manual - STV method', 'https://github.com/julien-boudry/Condorcet/blob/master/VOTING_METHODS.md#single-transferable-vote')]
|
||||
public static function make(
|
||||
#[FunctionParameter('Quota name')]
|
||||
string $quota
|
||||
): self {
|
||||
try {
|
||||
return match (mb_strtolower($quota)) {
|
||||
'droop quota', 'droop' => self::DROOP,
|
||||
'hare quota', 'hare' => self::HARE,
|
||||
'hagenbach-bischoff quota', 'hagenbach-bischoff' => self::HAGENBACH_BISCHOFF,
|
||||
'imperiali quota', 'imperiali' => self::IMPERIALI,
|
||||
};
|
||||
} catch (\UnhandledMatchError $e) {
|
||||
throw new StvQuotaNotImplementedException('"'.$quota.'"');
|
||||
}
|
||||
}
|
||||
|
||||
public function getQuota(int $votesWeight, int $seats): float
|
||||
{
|
||||
return match ($this) {
|
||||
self::DROOP => floor(($votesWeight / ($seats + 1)) + 1),
|
||||
self::HARE => $votesWeight / $seats,
|
||||
self::HAGENBACH_BISCHOFF => $votesWeight / ($seats + 1),
|
||||
self::IMPERIALI, => $votesWeight / ($seats + 2),
|
||||
};
|
||||
}
|
||||
}
|
74
include/condorcet/src/Algo/Tools/TieBreakersCollection.php
Normal file
74
include/condorcet/src/Algo/Tools/TieBreakersCollection.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?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);
|
||||
|
||||
/////////// TOOLS FOR MODULAR ALGORITHMS ///////////
|
||||
|
||||
namespace CondorcetPHP\Condorcet\Algo\Tools;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\InternalModulesAPI;
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
// Generic for Algorithms
|
||||
#[InternalModulesAPI]
|
||||
abstract class TieBreakersCollection
|
||||
{
|
||||
public static function electSomeLosersbasedOnPairwiseComparaison(Election $election, array $candidatesKeys): array
|
||||
{
|
||||
$pairwise = $election->getPairwise();
|
||||
$pairwiseStats = PairwiseStats::PairwiseComparison($pairwise);
|
||||
$tooKeep = [];
|
||||
|
||||
foreach ($candidatesKeys as $oneCandidateKeyTotest) {
|
||||
$select = true;
|
||||
foreach ($candidatesKeys as $oneChallengerKey) {
|
||||
if ($oneCandidateKeyTotest === $oneChallengerKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($pairwise[$oneCandidateKeyTotest]['win'][$oneChallengerKey] > $pairwise[$oneCandidateKeyTotest]['lose'][$oneChallengerKey] ||
|
||||
$pairwiseStats[$oneCandidateKeyTotest]['balance'] > $pairwiseStats[$oneChallengerKey]['balance'] ||
|
||||
$pairwiseStats[$oneCandidateKeyTotest]['win'] > $pairwiseStats[$oneChallengerKey]['win']
|
||||
) {
|
||||
$select = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($select) {
|
||||
$tooKeep[] = $oneCandidateKeyTotest;
|
||||
}
|
||||
}
|
||||
|
||||
return (\count($tooKeep) > 0) ? $tooKeep : $candidatesKeys;
|
||||
}
|
||||
|
||||
public static function tieBreakerWithAnotherMethods(Election $election, array $methods, array $candidatesKeys): array
|
||||
{
|
||||
foreach ($methods as $oneMethod) {
|
||||
$tooKeep = [];
|
||||
|
||||
$methodResults = $election->getResult($oneMethod)->getResultAsInternalKey();
|
||||
|
||||
foreach ($methodResults as $rankValue) {
|
||||
foreach ($rankValue as $oneCandidateKey) {
|
||||
if (\in_array($oneCandidateKey, $candidatesKeys, true)) {
|
||||
$tooKeep[] = $oneCandidateKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($tooKeep) > 0 && \count($tooKeep) !== \count($candidatesKeys)) {
|
||||
return $tooKeep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $candidatesKeys;
|
||||
}
|
||||
}
|
28
include/condorcet/src/Algo/Tools/VirtualVote.php
Normal file
28
include/condorcet/src/Algo/Tools/VirtualVote.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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\Algo\Tools;
|
||||
|
||||
use CondorcetPHP\Condorcet\Vote;
|
||||
|
||||
abstract class VirtualVote
|
||||
{
|
||||
public static function removeCandidates(Vote $vote, array $candidatesList): Vote
|
||||
{
|
||||
$virtualVote = clone $vote;
|
||||
|
||||
foreach ($candidatesList as $oneCandidate) {
|
||||
$virtualVote->removeCandidate($oneCandidate);
|
||||
}
|
||||
|
||||
return $virtualVote;
|
||||
}
|
||||
}
|
149
include/condorcet/src/Candidate.php
Normal file
149
include/condorcet/src/Candidate.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Throwable\{CandidateExistsException, CandidateInvalidNameException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, PublicAPI, Related, Throws};
|
||||
|
||||
class Candidate implements \Stringable
|
||||
{
|
||||
use Linkable;
|
||||
use CondorcetVersion;
|
||||
|
||||
private array $name = [];
|
||||
private bool $provisional = false;
|
||||
|
||||
// -------
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Build a candidate.')]
|
||||
#[Example('Manual - Create Candidates', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-A.-Create-an-Election-%23-2.-Create-Candidates')]
|
||||
#[Related('Candidate::setName')]
|
||||
public function __construct(
|
||||
#[FunctionParameter('Candidate Name')]
|
||||
string $name
|
||||
) {
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$this->link = null;
|
||||
|
||||
$r = get_object_vars($this);
|
||||
unset($r['link']);
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// SETTERS
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Change the candidate name.\n*If this will not cause conflicts if the candidate is already participating in elections and would namesake. This situation will throw an exception.*")]
|
||||
#[FunctionReturn('In case of success, return TRUE')]
|
||||
#[Throws(CandidateInvalidNameException::class)]
|
||||
public function setName(
|
||||
#[FunctionParameter('Candidate Name')]
|
||||
string $name
|
||||
): bool {
|
||||
$name = trim($name);
|
||||
|
||||
if (mb_strlen($name) > Election::MAX_CANDIDATE_NAME_LENGTH) {
|
||||
throw new CandidateInvalidNameException($name);
|
||||
}
|
||||
|
||||
if (preg_match('/<|>|\n|\t|\0|\^|\*|\$|:|;|(\|\|)|"|#/', $name) === 1) {
|
||||
throw new CandidateInvalidNameException($name);
|
||||
}
|
||||
|
||||
if (!$this->checkNameInElectionContext($name)) {
|
||||
throw new CandidateExistsException("the name '{$name}' is taken by another candidate");
|
||||
}
|
||||
|
||||
$this->name[] = ['name' => $name, 'timestamp' => microtime(true)];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setProvisionalState(bool $provisional): void
|
||||
{
|
||||
$this->provisional = $provisional;
|
||||
}
|
||||
|
||||
// GETTERS
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the candidate name.')]
|
||||
#[FunctionReturn('Candidate name.')]
|
||||
#[Related('Candidate::getHistory', 'Candidate::setName')]
|
||||
public function getName(): string
|
||||
{
|
||||
return end($this->name)['name'];
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return an history of each namming change, with timestamp.')]
|
||||
#[FunctionReturn('An explicit multi-dimenssional array.')]
|
||||
#[Related('Candidate::getCreateTimestamp')]
|
||||
public function getHistory(): array
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the timestamp corresponding of the creation of this candidate.')]
|
||||
#[FunctionReturn('Timestamp')]
|
||||
#[Related('Candidate::getTimestamp')]
|
||||
public function getCreateTimestamp(): float
|
||||
{
|
||||
return $this->name[0]['timestamp'];
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the timestamp corresponding of the last namming change.')]
|
||||
#[FunctionReturn('Timestamp')]
|
||||
#[Related('Candidate::getCreateTimestamp')]
|
||||
public function getTimestamp(): float
|
||||
{
|
||||
return end($this->name)['timestamp'];
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("When you create yourself the vote object, without use the Election::addVote or other native election method. And if you use string input (or array of string).\nThen, these string input will be converted to into temporary candidate objects, named \"provisional\". because you don't create the candidate yourself. They have a provisonal statut true.\nWhen the vote will be added for the first time to an election, provisional candidate object with a name that matches an election candidate, will be converted into the election candidate. And first ranking will be save into Vote history (Vote::getHistory).\n\nSee VoteTest::testVoteHistory() test for a demonstration. In principle this is transparent from a usage point of view. If you want to avoid any non-strict comparisons, however, you should prefer to create your votes with the Election object, or with Candidate Objects in input. But, you must never getback a candidate marked as provisional in an another election in the same time, it's will not working.")]
|
||||
#[FunctionReturn('True if candidate object is in a provisional state, false else.')]
|
||||
public function getProvisionalState(): bool
|
||||
{
|
||||
return $this->provisional;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// INTERNAL
|
||||
|
||||
private function checkNameInElectionContext(string $name): bool
|
||||
{
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
if (!$link->canAddCandidate($name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
236
include/condorcet/src/Condorcet.php
Normal file
236
include/condorcet/src/Condorcet.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\Throwable\AlgorithmException;
|
||||
|
||||
// Registering native Condorcet Methods implementation
|
||||
|
||||
# Classic Methods
|
||||
Condorcet::addMethod(Algo\Methods\Borda\BordaCount::class);
|
||||
Condorcet::addMethod(Algo\Methods\Copeland\Copeland::class);
|
||||
Condorcet::addMethod(Algo\Methods\Dodgson\DodgsonQuick::class);
|
||||
Condorcet::addMethod(Algo\Methods\Dodgson\DodgsonTidemanApproximation::class);
|
||||
Condorcet::addMethod(Algo\Methods\Borda\DowdallSystem::class);
|
||||
Condorcet::addMethod(Algo\Methods\InstantRunoff\InstantRunoff::class);
|
||||
Condorcet::addMethod(Algo\Methods\KemenyYoung\KemenyYoung::class);
|
||||
Condorcet::addMethod(Algo\Methods\Majority\FirstPastThePost::class);
|
||||
Condorcet::addMethod(Algo\Methods\Majority\MultipleRoundsSystem::class);
|
||||
Condorcet::addMethod(Algo\Methods\Minimax\MinimaxWinning::class);
|
||||
Condorcet::addMethod(Algo\Methods\Minimax\MinimaxMargin::class);
|
||||
Condorcet::addMethod(Algo\Methods\Minimax\MinimaxOpposition::class);
|
||||
Condorcet::addMethod(Algo\Methods\RankedPairs\RankedPairsMargin::class);
|
||||
Condorcet::addMethod(Algo\Methods\RankedPairs\RankedPairsWinning::class);
|
||||
Condorcet::addMethod(Algo\Methods\Schulze\SchulzeWinning::class);
|
||||
Condorcet::addMethod(Algo\Methods\Schulze\SchulzeMargin::class);
|
||||
Condorcet::addMethod(Algo\Methods\Schulze\SchulzeRatio::class);
|
||||
|
||||
# Proportional Methods
|
||||
Condorcet::addMethod(Algo\Methods\STV\SingleTransferableVote::class);
|
||||
Condorcet::addMethod(Algo\Methods\STV\CPO_STV::class);
|
||||
Condorcet::addMethod(Algo\Methods\LargestRemainder\LargestRemainder::class);
|
||||
Condorcet::addMethod(Algo\Methods\HighestAverages\Jefferson::class);
|
||||
Condorcet::addMethod(Algo\Methods\HighestAverages\SainteLague::class);
|
||||
|
||||
// Set the default Condorcet Class algorithm
|
||||
Condorcet::setDefaultMethod('Schulze');
|
||||
|
||||
abstract class Condorcet
|
||||
{
|
||||
/////////// CONSTANTS ///////////
|
||||
final public const AUTHOR = 'Julien Boudry and contributors';
|
||||
final public const HOMEPAGE = 'https://github.com/julien-boudry/Condorcet';
|
||||
|
||||
#[PublicAPI]
|
||||
final public const VERSION = '4.2.0';
|
||||
|
||||
#[PublicAPI]
|
||||
final public const CONDORCET_BASIC_CLASS = Algo\Methods\CondorcetBasic::class;
|
||||
|
||||
protected static ?string $defaultMethod = null;
|
||||
protected static array $authMethods = [self::CONDORCET_BASIC_CLASS => (Algo\Methods\CondorcetBasic::class)::METHOD_NAME];
|
||||
|
||||
public static bool $UseTimer = false;
|
||||
|
||||
|
||||
/////////// STATICS METHODS ///////////
|
||||
|
||||
// Return library version number
|
||||
#[PublicAPI]
|
||||
#[Description('Get the library version.')]
|
||||
#[FunctionReturn('Condorcet PHP version.')]
|
||||
#[Related('Election::getObjectVersion')]
|
||||
public static function getVersion(
|
||||
#[FunctionParameter("* true will return : '2.0'\n* false will return : '2.0.0'")]
|
||||
bool $major = false
|
||||
): string {
|
||||
if ($major === true) {
|
||||
$version = explode('.', self::VERSION);
|
||||
return $version[0].'.'.$version[1];
|
||||
} else {
|
||||
return self::VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
// Return an array with auth methods
|
||||
#[PublicAPI]
|
||||
#[Description('Get a list of supported algorithm.')]
|
||||
#[FunctionReturn('Populated by method string name. You can use it on getResult ... and others methods.')]
|
||||
#[Related('static Condorcet::isAuthMethod', 'static Condorcet::getMethodClass')]
|
||||
public static function getAuthMethods(
|
||||
#[FunctionParameter('Include or not the natural Condorcet base algorithm')]
|
||||
bool $basic = false
|
||||
): array {
|
||||
$auth = self::$authMethods;
|
||||
|
||||
// Don't show Natural Condorcet
|
||||
if (!$basic) {
|
||||
unset($auth[self::CONDORCET_BASIC_CLASS]);
|
||||
}
|
||||
|
||||
return array_column($auth, 0);
|
||||
}
|
||||
|
||||
|
||||
// Return the Class default method
|
||||
#[PublicAPI]
|
||||
#[Description('Return the Condorcet static default method.')]
|
||||
#[FunctionReturn('Method name.')]
|
||||
#[Related('static Condorcet::getAuthMethods', 'static Condorcet::setDefaultMethod')]
|
||||
public static function getDefaultMethod(): ?string
|
||||
{
|
||||
return self::$defaultMethod;
|
||||
}
|
||||
|
||||
|
||||
// Check if the method is supported
|
||||
#[PublicAPI]
|
||||
#[Description('Return the full class path for a method.')]
|
||||
#[FunctionReturn('Return null is method not exist.')]
|
||||
#[Throws(AlgorithmException::class)]
|
||||
#[Related('static Condorcet::getAuthMethods')]
|
||||
public static function getMethodClass(
|
||||
#[FunctionParameter('A valid method name')]
|
||||
string $method
|
||||
): ?string {
|
||||
$auth = self::$authMethods;
|
||||
|
||||
if (empty($method)) {
|
||||
throw new AlgorithmException('no method name given');
|
||||
}
|
||||
|
||||
if (isset($auth[$method])) {
|
||||
return $method;
|
||||
} else { // Alias
|
||||
foreach ($auth as $class => $alias) {
|
||||
foreach ($alias as $entry) {
|
||||
if (strcasecmp($method, $entry) === 0) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Test if a method is in the result set of Condorcet::getAuthMethods.')]
|
||||
#[FunctionReturn('True / False')]
|
||||
#[Related('static Condorcet::getMethodClass', 'static Condorcet::getAuthMethods')]
|
||||
public static function isAuthMethod(
|
||||
#[FunctionParameter('A valid method name or class')]
|
||||
string $method
|
||||
): bool {
|
||||
return self::getMethodClass($method) !== null;
|
||||
}
|
||||
|
||||
|
||||
// Add algos
|
||||
#[PublicAPI]
|
||||
#[Description('If you create your own Condorcet Algo. You will need it !')]
|
||||
#[FunctionReturn('True on Success. False on failure.')]
|
||||
#[Related('static Condorcet::isAuthMethod', 'static Condorcet::getMethodClass')]
|
||||
public static function addMethod(
|
||||
#[FunctionParameter('The class name implementing your method. The class name includes the namespace it was declared in (e.g. Foo\Bar).')]
|
||||
string $methodClass
|
||||
): bool {
|
||||
// Check algos
|
||||
if (self::isAuthMethod($methodClass) || !self::testMethod($methodClass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adding algo
|
||||
self::$authMethods[$methodClass] = $methodClass::METHOD_NAME;
|
||||
|
||||
if (self::getDefaultMethod() === null) {
|
||||
self::setDefaultMethod($methodClass);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if the class Algo. exist and ready to be used
|
||||
protected static function testMethod(string $method): bool
|
||||
{
|
||||
if (!class_exists($method)) {
|
||||
throw new AlgorithmException("no class found for '{$method}'");
|
||||
}
|
||||
|
||||
if (!is_subclass_of($method, Algo\MethodInterface::class) || !is_subclass_of($method, Algo\Method::class)) {
|
||||
throw new AlgorithmException('the given class is not correct');
|
||||
}
|
||||
|
||||
foreach ($method::METHOD_NAME as $alias) {
|
||||
if (self::isAuthMethod($alias)) {
|
||||
throw new AlgorithmException('the given class is using an existing alias');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Change default method for this class.
|
||||
#[PublicAPI]
|
||||
#[Description('Put a new static method by default for the news Condorcet objects.')]
|
||||
#[FunctionReturn('In case of success, return TRUE')]
|
||||
#[Related('static Condorcet::getDefaultMethod')]
|
||||
public static function setDefaultMethod(
|
||||
#[FunctionParameter('A valid method name or class')]
|
||||
string $method
|
||||
): bool {
|
||||
if (($method = self::getMethodClass($method)) && $method !== self::CONDORCET_BASIC_CLASS) {
|
||||
self::$defaultMethod = $method;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function condorcetBasicSubstitution(?string $substitution): string
|
||||
{
|
||||
if ($substitution !== null) {
|
||||
if (self::isAuthMethod($substitution)) {
|
||||
$algo = $substitution;
|
||||
} else {
|
||||
throw new AlgorithmException("No class found for method '{$substitution}'");
|
||||
}
|
||||
} else {
|
||||
$algo = self::CONDORCET_BASIC_CLASS;
|
||||
}
|
||||
|
||||
return $algo;
|
||||
}
|
||||
}
|
37
include/condorcet/src/CondorcetVersion.php
Normal file
37
include/condorcet/src/CondorcetVersion.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related};
|
||||
|
||||
// Generic for many Condorcet Class
|
||||
trait CondorcetVersion
|
||||
{
|
||||
// Build by Version
|
||||
protected string $objectVersion = Condorcet::VERSION;
|
||||
|
||||
#[PublicAPI('Candidate', 'Election', 'Result', 'Vote', "Algo\Pairwise", "DataManager\VotesManager", "Timer\Manager")]
|
||||
#[Description('Get the Condorcet PHP version who built this Election object. Usefull pour serializing Election.')]
|
||||
#[FunctionReturn('Condorcet PHP version.')]
|
||||
#[Related('static Condorcet::getVersion')]
|
||||
public function getObjectVersion(
|
||||
#[FunctionParameter("true will return : '2.0' and false will return : '2.0.0'")]
|
||||
bool $major = false
|
||||
): string {
|
||||
if ($major === true) {
|
||||
$version = explode('.', $this->objectVersion);
|
||||
return $version[0].'.'.$version[1];
|
||||
} else {
|
||||
return $this->objectVersion;
|
||||
}
|
||||
}
|
||||
}
|
3
include/condorcet/src/Console/Assets/Sources.md
Normal file
3
include/condorcet/src/Console/Assets/Sources.md
Normal 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)_
|
7
include/condorcet/src/Console/Assets/logo.125c.ascii
Normal file
7
include/condorcet/src/Console/Assets/logo.125c.ascii
Normal file
@ -0,0 +1,7 @@
|
||||
<condor1> _/_/_/ _/ _/ </><condor2> _/_/_/ _/ _/ _/_/_/</>
|
||||
<condor1> _/ _/_/ _/_/_/ _/_/_/ _/_/ _/ _/_/ _/_/_/ _/_/ _/_/_/_/ </><condor2> _/ _/ _/ _/ _/ _/</>
|
||||
<condor1> _/ _/ _/ _/ _/ _/ _/ _/ _/ _/_/ _/ _/_/_/_/ _/ </><condor2> _/_/_/ _/_/_/_/ _/_/_/</>
|
||||
<condor1>_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ </><condor2> _/ _/ _/ _/</>
|
||||
<condor1> _/_/_/ _/_/ _/ _/ _/_/_/ _/_/ _/ _/_/_/ _/_/_/ _/_/ </><condor2> _/ _/ _/ _/</>
|
||||
|
||||
<condor1>========================================================================================== </><condor2> ===============================</>
|
5
include/condorcet/src/Console/Assets/logo.73c.ascii
Normal file
5
include/condorcet/src/Console/Assets/logo.73c.ascii
Normal file
@ -0,0 +1,5 @@
|
||||
<condor1> ______ __ __ </><condor2> ____ __ ______ </>
|
||||
<condor1> / ____/___ ____ ____/ /___ _____________ / /_</><condor2> / __ \/ / / / __ \ </>
|
||||
<condor1> / / / __ \/ __ \/ __ / __ \/ ___/ ___/ _ \/ __/</><condor2> / /_/ / /_/ / /_/ /</>
|
||||
<condor1>/ /___/ /_/ / / / / /_/ / /_/ / / / /__/ __/ /_ </><condor2> / ____/ __ / ____/ </>
|
||||
<condor1>\____/\____/_/ /_/\__,_/\____/_/ \___/\___/\__/ </><condor2>/_/ /_/ /_/_/ </>
|
7
include/condorcet/src/Console/Assets/logo.90c.ascii
Normal file
7
include/condorcet/src/Console/Assets/logo.90c.ascii
Normal file
@ -0,0 +1,7 @@
|
||||
<condor1> _/_/_/ _/ _/ </>
|
||||
<condor1> _/ _/_/ _/_/_/ _/_/_/ _/_/ _/ _/_/ _/_/_/ _/_/ _/_/_/_/</>
|
||||
<condor1> _/ _/ _/ _/ _/ _/ _/ _/ _/ _/_/ _/ _/_/_/_/ _/ </>
|
||||
<condor1>_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ </>
|
||||
<condor1> _/_/_/ _/_/ _/ _/ _/_/_/ _/_/ _/ _/_/_/ _/_/_/ _/_/ </>
|
||||
|
||||
<condor1>==========================================================================================</>
|
803
include/condorcet/src/Console/Commands/ElectionCommand.php
Normal file
803
include/condorcet/src/Console/Commands/ElectionCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
97
include/condorcet/src/Console/CondorcetApplication.php
Normal file
97
include/condorcet/src/Console/CondorcetApplication.php
Normal 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;
|
||||
}
|
||||
}
|
31
include/condorcet/src/Console/Helper/CommandInputHelper.php
Normal file
31
include/condorcet/src/Console/Helper/CommandInputHelper.php
Normal 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)));
|
||||
}
|
||||
}
|
67
include/condorcet/src/Console/Helper/FormaterHelper.php
Normal file
67
include/condorcet/src/Console/Helper/FormaterHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
163
include/condorcet/src/Console/Style/CondorcetStyle.php
Normal file
163
include/condorcet/src/Console/Style/CondorcetStyle.php
Normal 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}</>");
|
||||
}
|
||||
}
|
30
include/condorcet/src/Constraints/NoTie.php
Normal file
30
include/condorcet/src/Constraints/NoTie.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\Constraints;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Election, Vote, VoteConstraintInterface};
|
||||
|
||||
class NoTie implements VoteConstraintInterface
|
||||
{
|
||||
public static function isVoteAllow(Election $election, Vote $vote): bool
|
||||
{
|
||||
$voteRanking = $vote->getContextualRankingWithoutSort($election);
|
||||
|
||||
foreach ($voteRanking as $oneRank) {
|
||||
if (\count($oneRank) > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
415
include/condorcet/src/DataManager/ArrayManager.php
Normal file
415
include/condorcet/src/DataManager/ArrayManager.php
Normal file
@ -0,0 +1,415 @@
|
||||
<?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\DataManager;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\Throws;
|
||||
use CondorcetPHP\Condorcet\{CondorcetVersion, Election, Vote};
|
||||
use CondorcetPHP\Condorcet\Throwable\DataHandlerException;
|
||||
use CondorcetPHP\Condorcet\DataManager\DataHandlerDrivers\DataHandlerDriverInterface;
|
||||
|
||||
abstract class ArrayManager implements \ArrayAccess, \Countable, \Iterator
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
public static int $CacheSize = 2000;
|
||||
public static int $MaxContainerLength = 2000;
|
||||
|
||||
protected array $Container = [];
|
||||
protected ?DataHandlerDriverInterface $DataHandler = null;
|
||||
protected \WeakReference $Election;
|
||||
|
||||
protected array $Cache = [];
|
||||
protected int $CacheMaxKey = 0;
|
||||
protected int $CacheMinKey = 0;
|
||||
|
||||
protected ?int $cursor = null;
|
||||
protected int $counter = 0;
|
||||
protected int $maxKey = -1;
|
||||
|
||||
public function __construct(Election $election)
|
||||
{
|
||||
$this->setElection($election);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->regularize();
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$this->regularize();
|
||||
$this->clearCache();
|
||||
$this->rewind();
|
||||
|
||||
return ['Container' => $this->Container, 'DataHandler' => $this->DataHandler];
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->Container = $data['Container'];
|
||||
$this->DataHandler = $data['DataHandler'];
|
||||
|
||||
$this->resetMaxKey();
|
||||
$this->resetCounter();
|
||||
}
|
||||
|
||||
public function getElection(): Election
|
||||
{
|
||||
return $this->Election->get();
|
||||
}
|
||||
|
||||
public function setElection(Election $election): void
|
||||
{
|
||||
$this->Election = \WeakReference::create($election);
|
||||
}
|
||||
|
||||
|
||||
/////////// Implement ArrayAccess ///////////
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if ($offset === null) {
|
||||
$this->Container[++$this->maxKey] = $value;
|
||||
++$this->counter;
|
||||
} else {
|
||||
$state = $this->keyExist($offset);
|
||||
$this->Container[$offset] = $value;
|
||||
|
||||
if (!$state) {
|
||||
++$this->counter;
|
||||
|
||||
if ($offset > $this->maxKey) {
|
||||
$this->maxKey = $offset;
|
||||
}
|
||||
|
||||
ksort($this->Container, \SORT_NUMERIC);
|
||||
} elseif ($this->DataHandler !== null) {
|
||||
$this->DataHandler->deleteOneEntity(key: $offset, justTry: true);
|
||||
unset($this->Cache[$offset]);
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
// Delegate this step to VoteManager. To give him time to calculate the pairwise iteratively. Without recharging the memory.
|
||||
// $this->checkRegularize();
|
||||
}
|
||||
|
||||
// Use by isset() function, must return false if offset value is null.
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->Container[$offset]) || ($this->DataHandler !== null && $this->DataHandler->selectOneEntity(key: $offset) !== false);
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
if ($this->keyExist($offset)) {
|
||||
if (\array_key_exists(key: $offset, array: $this->Container)) {
|
||||
$this->preDeletedTask($this->Container[$offset]);
|
||||
unset($this->Container[$offset]);
|
||||
} else {
|
||||
if (\array_key_exists(key: $offset, array: $this->Cache)) {
|
||||
$this->preDeletedTask($this->Cache[$offset]);
|
||||
unset($this->Cache[$offset]);
|
||||
}
|
||||
|
||||
$this->DataHandler->deleteOneEntity(key: $offset, justTry: false);
|
||||
}
|
||||
|
||||
--$this->counter;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
if (isset($this->Container[$offset])) {
|
||||
return $this->Container[$offset];
|
||||
} elseif ($this->DataHandler !== null) {
|
||||
if (\array_key_exists(key: $offset, array: $this->Cache)) {
|
||||
return $this->Cache[$offset];
|
||||
} else {
|
||||
$oneEntity = $this->DataHandler->selectOneEntity(key: $offset);
|
||||
if ($oneEntity === false) {
|
||||
return null;
|
||||
} else {
|
||||
return $this->Cache[$offset] = $this->decodeOneEntity($oneEntity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// Implement Iterator ///////////
|
||||
|
||||
protected bool $valid = true;
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->cursor = null;
|
||||
$this->valid = true;
|
||||
|
||||
reset($this->Cache);
|
||||
reset($this->Container);
|
||||
}
|
||||
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->offsetGet($this->key());
|
||||
}
|
||||
|
||||
public function key(): ?int
|
||||
{
|
||||
if ($this->counter === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return $this->cursor ?? $this->getFirstKey();
|
||||
}
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
$oldCursor = $this->cursor;
|
||||
|
||||
if ($this->cursor >= $this->maxKey) {
|
||||
// Do nothing
|
||||
} elseif (!$this->isUsingHandler()) {
|
||||
$this->setCursorOnNextKeyInArray($this->Container);
|
||||
} else {
|
||||
$this->populateCache();
|
||||
$this->setCursorOnNextKeyInArray($this->Cache);
|
||||
}
|
||||
|
||||
if ($this->cursor === $oldCursor) {
|
||||
$this->valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function setCursorOnNextKeyInArray(array &$array): void
|
||||
{
|
||||
next($array);
|
||||
$arrayKey = key($array);
|
||||
|
||||
if ($arrayKey > $this->key()) {
|
||||
$this->cursor = $arrayKey;
|
||||
}
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return ($this->counter !== 0) ? $this->valid : false;
|
||||
}
|
||||
|
||||
|
||||
/////////// Implement Countable ///////////
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->counter;
|
||||
}
|
||||
|
||||
/////////// Array Methods ///////////
|
||||
|
||||
public function getFullDataSet(): array
|
||||
{
|
||||
if ($this->isUsingHandler()) {
|
||||
$this->regularize();
|
||||
$this->clearCache();
|
||||
|
||||
return $this->Cache = $this->decodeManyEntities($this->DataHandler->selectRangeEntities(key: 0, limit: $this->maxKey + 1));
|
||||
} else {
|
||||
return $this->Container;
|
||||
}
|
||||
}
|
||||
|
||||
public function keyExist(int $offset): bool
|
||||
{
|
||||
if (\array_key_exists(key: $offset, array: $this->Container) || ($this->DataHandler !== null && $this->DataHandler->selectOneEntity(key: $offset) !== false)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFirstKey(): int
|
||||
{
|
||||
$r = array_keys($this->Container);
|
||||
|
||||
if ($this->DataHandler !== null) {
|
||||
$r[] = $this->DataHandler->selectMinKey();
|
||||
}
|
||||
|
||||
return (int) min($r);
|
||||
}
|
||||
|
||||
public function getContainerSize(): int
|
||||
{
|
||||
return \count($this->Container);
|
||||
}
|
||||
|
||||
public function getCacheSize(): int
|
||||
{
|
||||
return \count($this->Cache);
|
||||
}
|
||||
|
||||
public function debugGetCache(): array
|
||||
{
|
||||
return $this->Cache;
|
||||
}
|
||||
|
||||
|
||||
/////////// HANDLER API ///////////
|
||||
|
||||
abstract protected function preDeletedTask(Vote $object): void;
|
||||
|
||||
abstract protected function decodeOneEntity(string $data): Vote;
|
||||
|
||||
abstract protected function encodeOneEntity(Vote $data): string;
|
||||
|
||||
protected function decodeManyEntities(array $entities): array
|
||||
{
|
||||
$r = [];
|
||||
|
||||
foreach ($entities as $key => $oneEntity) {
|
||||
$r[(int) $key] = $this->decodeOneEntity($oneEntity);
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
protected function encodeManyEntities(array $entities): array
|
||||
{
|
||||
$r = [];
|
||||
|
||||
foreach ($entities as $key => $oneEntity) {
|
||||
$r[(int) $key] = $this->encodeOneEntity($oneEntity);
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
public function regularize(): bool
|
||||
{
|
||||
if (!$this->isUsingHandler() || empty($this->Container)) {
|
||||
return false;
|
||||
} else {
|
||||
$this->DataHandler->insertEntities($this->encodeManyEntities($this->Container));
|
||||
$this->Container = [];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkRegularize(): bool
|
||||
{
|
||||
if ($this->DataHandler !== null && self::$MaxContainerLength <= $this->getContainerSize()) {
|
||||
$this->regularize();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function populateCache(): void
|
||||
{
|
||||
$this->regularize();
|
||||
|
||||
$currentKey = $this->key();
|
||||
|
||||
if (empty($this->Cache) || $currentKey >= $this->CacheMaxKey || $currentKey < $this->CacheMinKey) {
|
||||
$this->clearCache();
|
||||
$this->Cache = $this->decodeManyEntities($this->DataHandler->selectRangeEntities(key: $currentKey, limit: self::$CacheSize));
|
||||
|
||||
$keys = array_keys($this->Cache);
|
||||
$this->CacheMaxKey = max($keys);
|
||||
$this->CacheMinKey = min($keys);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
foreach ($this->Cache as $e) {
|
||||
$this->preDeletedTask($e);
|
||||
}
|
||||
|
||||
$this->Cache = [];
|
||||
$this->CacheMaxKey = 0;
|
||||
$this->CacheMinKey = 0;
|
||||
}
|
||||
|
||||
public function isUsingHandler(): bool
|
||||
{
|
||||
return $this->DataHandler !== null;
|
||||
}
|
||||
|
||||
/////////// HANDLER INTERRACTION ///////////
|
||||
|
||||
public function resetCounter(): int
|
||||
{
|
||||
return $this->counter = $this->getContainerSize() + ($this->isUsingHandler() ? $this->DataHandler->countEntities() : 0);
|
||||
}
|
||||
|
||||
public function resetMaxKey(): ?int
|
||||
{
|
||||
$this->resetCounter();
|
||||
|
||||
if ($this->count() < 1) {
|
||||
$this->maxKey = -1;
|
||||
return null;
|
||||
} else {
|
||||
$maxContainerKey = empty($this->Container) ? null : max(array_keys($this->Container));
|
||||
$maxHandlerKey = $this->DataHandler !== null ? $this->DataHandler->selectMaxKey() : null;
|
||||
|
||||
return $this->maxKey = max($maxContainerKey, $maxHandlerKey);
|
||||
}
|
||||
}
|
||||
|
||||
#[Throws(DataHandlerException::class)]
|
||||
public function importHandler(DataHandlerDriverInterface $handler): bool
|
||||
{
|
||||
if ($handler->countEntities() === 0) {
|
||||
$this->DataHandler = $handler;
|
||||
|
||||
try {
|
||||
$this->regularize();
|
||||
} catch (\Exception $e) {
|
||||
$this->DataHandler = null;
|
||||
$this->resetCounter();
|
||||
$this->resetMaxKey();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->resetCounter();
|
||||
$this->resetMaxKey();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new DataHandlerException;
|
||||
}
|
||||
}
|
||||
|
||||
public function closeHandler(): void
|
||||
{
|
||||
if ($this->DataHandler !== null) {
|
||||
$this->regularize();
|
||||
$this->clearCache();
|
||||
|
||||
$this->Container = $this->decodeManyEntities($this->DataHandler->selectRangeEntities(key: 0, limit: $this->maxKey + 1));
|
||||
|
||||
$this->DataHandler = null;
|
||||
|
||||
$this->resetCounter();
|
||||
$this->resetMaxKey();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?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\DataManager\DataHandlerDrivers;
|
||||
|
||||
interface DataHandlerDriverInterface
|
||||
{
|
||||
/* public $dataContextObject; */
|
||||
// The Data Manager will set an object into this property. You should call for each Entity $dataContextObject->dataCallBack($EntityData) before returning stored data by the two select method.
|
||||
|
||||
|
||||
// Entities to register.
|
||||
// Ex: [Condorcet/Vote,Condorcet/Vote,Condorcet/Vote]. The key should not be kept
|
||||
public function insertEntities(array $input): void;
|
||||
|
||||
// Delete Entity with this key. If justTry is true, don't throw Exception if row not exist. Else throw an \CondorcetPHP\Concordet\Throwable\CondorcetInternalError.
|
||||
public function deleteOneEntity(int $key, bool $justTry): ?int;
|
||||
|
||||
// Return (int) max register key.
|
||||
// SQL example : SELECT max(key) FROM...
|
||||
public function selectMaxKey(): ?int;
|
||||
|
||||
// Return (int) max register key.
|
||||
// SQL example : SELECT min(key) FROM...
|
||||
public function selectMinKey(): ?int;
|
||||
|
||||
// Return (int) :nomber of recording
|
||||
// SQL example : SELECT count(*) FROM...
|
||||
public function countEntities(): int;
|
||||
|
||||
// Return one Entity by key
|
||||
public function selectOneEntity(int $key): string|bool;
|
||||
|
||||
// Return an array of entity where $key is the first Entity and $limit is the maximum number of entity. Must return an array, keys must be preseve into there.
|
||||
// Arg example : (42, 3)
|
||||
// Return example : [42 => Condorcet/Vote, 43 => Condorcet/Vote, 44 => Condorcet/Vote]
|
||||
public function selectRangeEntities(int $key, int $limit): array;
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
<?php
|
||||
/*
|
||||
PDO DataHandler Module - From the original Condorcet 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\DataManager\DataHandlerDrivers\PdoDriver;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{PublicAPI, Throws};
|
||||
use CondorcetPHP\Condorcet\CondorcetVersion;
|
||||
use CondorcetPHP\Condorcet\DataManager\DataHandlerDrivers\DataHandlerDriverInterface;
|
||||
use CondorcetPHP\Condorcet\Throwable\DataHandlerException;
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\CondorcetInternalError;
|
||||
|
||||
class PdoHandlerDriver implements DataHandlerDriverInterface
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
#[PublicAPI]
|
||||
public const SEGMENT = [499, 50, 4, 1]; // Must be ordered desc.
|
||||
|
||||
protected \PDO $handler;
|
||||
protected bool $transaction = false;
|
||||
protected bool $queryError = false;
|
||||
|
||||
// Database structure
|
||||
#[PublicAPI]
|
||||
public static bool $preferBlobInsteadVarchar = true;
|
||||
|
||||
protected array $struct;
|
||||
// Prepare Query
|
||||
protected array $prepare = [];
|
||||
|
||||
|
||||
#[Throws(DataHandlerException::class)]
|
||||
public function __construct(\PDO $bdd, bool $tryCreateTable = false, array $struct = ['tableName' => 'Entities', 'primaryColumnName' => 'id', 'dataColumnName' => 'data'])
|
||||
{
|
||||
if (!$this->checkStructureTemplate($struct)) {
|
||||
throw new DataHandlerException('invalid structure template for PdoHandler');
|
||||
}
|
||||
|
||||
$this->struct = $struct;
|
||||
|
||||
$this->handler = $bdd;
|
||||
|
||||
$this->handler->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
if ($tryCreateTable) {
|
||||
$this->createTable();
|
||||
}
|
||||
|
||||
$this->initPrepareQuery();
|
||||
}
|
||||
|
||||
// INTERNAL
|
||||
|
||||
protected function checkStructureTemplate(array &$struct): bool
|
||||
{
|
||||
if (!empty($struct['tableName']) && !empty($struct['primaryColumnName']) && !empty($struct['dataColumnName']) &&
|
||||
\is_string($struct['tableName']) && \is_string($struct['primaryColumnName']) && \is_string($struct['dataColumnName'])
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function createTable(): void
|
||||
{
|
||||
$dataType = (self::$preferBlobInsteadVarchar) ? 'BLOB' : 'VARCHAR';
|
||||
|
||||
$tableCreationQuery = match ($this->handler->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
|
||||
default => 'CREATE TABLE IF NOT EXISTS '.$this->struct['tableName'].' ('.$this->struct['primaryColumnName'].' INT AUTO_INCREMENT PRIMARY KEY NOT NULL , '.$this->struct['dataColumnName'].' '.$dataType.' NOT NULL );'
|
||||
};
|
||||
|
||||
try {
|
||||
$this->handler->exec($tableCreationQuery);
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function initPrepareQuery(): void
|
||||
{
|
||||
$template = [];
|
||||
|
||||
// Base - Small query ends
|
||||
$template['end_template'] = ';';
|
||||
$template['insert_template'] = 'INSERT INTO '.$this->struct['tableName'].' ('.$this->struct['primaryColumnName'].', '.$this->struct['dataColumnName'].') VALUES ';
|
||||
$template['delete_template'] = 'DELETE FROM '.$this->struct['tableName'].' WHERE '.$this->struct['primaryColumnName'];
|
||||
$template['select_template'] = 'SELECT '.$this->struct['primaryColumnName'].','.$this->struct['dataColumnName'].' FROM '.$this->struct['tableName'].' WHERE '.$this->struct['primaryColumnName'];
|
||||
|
||||
// Select the max / min key value. Usefull if array cursor is lost on DataManager.
|
||||
$this->prepare['selectMaxKey'] = $this->handler->prepare('SELECT max('.$this->struct['primaryColumnName'].') FROM '.$this->struct['tableName'] . $template['end_template']);
|
||||
$this->prepare['selectMinKey'] = $this->handler->prepare('SELECT min('.$this->struct['primaryColumnName'].') FROM '.$this->struct['tableName'] . $template['end_template']);
|
||||
|
||||
// Insert many Entities
|
||||
$makeMany = static function (int $how) use (&$template): string {
|
||||
$query = $template['insert_template'];
|
||||
|
||||
for ($i=1; $i < $how; $i++) {
|
||||
$query .= '(:key'.$i.', :data'.$i.'),';
|
||||
}
|
||||
|
||||
$query .= '(:key'.$how.', :data'.$how.')' . $template['end_template'];
|
||||
|
||||
return $query;
|
||||
};
|
||||
|
||||
foreach (self::SEGMENT as $value) {
|
||||
$this->prepare['insert'.$value.'Entities'] = $this->handler->prepare($makeMany($value));
|
||||
}
|
||||
|
||||
// Delete one Entity
|
||||
$this->prepare['deleteOneEntity'] = $this->handler->prepare($template['delete_template'] . ' = ?' . $template['end_template']);
|
||||
|
||||
// Get a Entity
|
||||
$this->prepare['selectOneEntity'] = $this->handler->prepare($template['select_template'] . ' = ?' . $template['end_template']);
|
||||
|
||||
// Get a range of Entity
|
||||
$this->prepare['selectRangeEntities'] = $this->handler->prepare($template['select_template'] . ' >= :startKey order by '.$this->struct['primaryColumnName'].' asc LIMIT :limit' . $template['end_template']);
|
||||
|
||||
// Count Entities
|
||||
$this->prepare['countEntities'] = $this->handler->prepare('SELECT count('.$this->struct['primaryColumnName'].') FROM '. $this->struct['tableName'] . $template['end_template']);
|
||||
}
|
||||
|
||||
protected function initTransaction(): void
|
||||
{
|
||||
if (!$this->transaction) {
|
||||
$this->transaction = $this->handler->beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public function closeTransaction(): void
|
||||
{
|
||||
if ($this->transaction === true) {
|
||||
|
||||
/**
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
if ($this->queryError) {
|
||||
throw new CondorcetInternalError('Query Error.');
|
||||
}
|
||||
|
||||
$this->transaction = !$this->handler->commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DATA MANAGER
|
||||
public function insertEntities(array $input): void
|
||||
{
|
||||
$this->sliceInput($input);
|
||||
|
||||
try {
|
||||
$this->initTransaction();
|
||||
|
||||
foreach ($input as $group) {
|
||||
$param = [];
|
||||
$i = 1;
|
||||
$group_count = \count($group);
|
||||
|
||||
foreach ($group as $key => &$Entity) {
|
||||
$param['key'.$i] = $key;
|
||||
$param['data'.$i++] = $Entity;
|
||||
}
|
||||
|
||||
$this->prepare['insert'.$group_count.'Entities']->execute(
|
||||
$param
|
||||
);
|
||||
|
||||
/**
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
if ($this->prepare['insert'.$group_count.'Entities']->rowCount() !== $group_count) {
|
||||
throw new CondorcetInternalError('Not all entities have been inserted');
|
||||
}
|
||||
|
||||
$this->prepare['insert'.$group_count.'Entities']->closeCursor();
|
||||
}
|
||||
|
||||
$this->closeTransaction();
|
||||
} catch (\Throwable $e) {
|
||||
$this->queryError = true;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function sliceInput(array &$input): void
|
||||
{
|
||||
$count = \count($input);
|
||||
|
||||
foreach (self::SEGMENT as $value) {
|
||||
if ($count >= $value) {
|
||||
$input = array_chunk($input, $value, true);
|
||||
|
||||
$end = end($input);
|
||||
if (\count($input) > 1 && \count($end) < $value) {
|
||||
$this->sliceInput($end);
|
||||
unset($input[key($input)]);
|
||||
$input = array_merge($input, $end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function deleteOneEntity(int $key, bool $justTry): ?int
|
||||
{
|
||||
try {
|
||||
$this->prepare['deleteOneEntity']->bindParam(1, $key, \PDO::PARAM_INT);
|
||||
$this->prepare['deleteOneEntity']->execute();
|
||||
|
||||
$deleteCount = $this->prepare['deleteOneEntity']->rowCount();
|
||||
|
||||
/**
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
if (!$justTry && $deleteCount !== 1) {
|
||||
throw new CondorcetInternalError('Entity deletion failure.');
|
||||
}
|
||||
|
||||
$this->prepare['deleteOneEntity']->closeCursor();
|
||||
|
||||
return $deleteCount;
|
||||
} catch (\Throwable $e) {
|
||||
$this->queryError = true;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectMaxKey(): ?int
|
||||
{
|
||||
if ($this->countEntities() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->prepare['selectMaxKey']->execute();
|
||||
$r = (int) $this->prepare['selectMaxKey']->fetch(\PDO::FETCH_NUM)[0];
|
||||
$this->prepare['selectMaxKey']->closeCursor();
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
public function selectMinKey(): int
|
||||
{
|
||||
$this->prepare['selectMinKey']->execute();
|
||||
$r = (int) $this->prepare['selectMinKey']->fetch(\PDO::FETCH_NUM)[0];
|
||||
$this->prepare['selectMinKey']->closeCursor();
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
public function countEntities(): int
|
||||
{
|
||||
$this->prepare['countEntities']->execute();
|
||||
$r = (int) $this->prepare['countEntities']->fetch(\PDO::FETCH_NUM)[0];
|
||||
$this->prepare['countEntities']->closeCursor();
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
// return false if Entity does not exist.
|
||||
public function selectOneEntity(int $key): string|bool
|
||||
{
|
||||
$this->prepare['selectOneEntity']->bindParam(1, $key, \PDO::PARAM_INT);
|
||||
$this->prepare['selectOneEntity']->execute();
|
||||
|
||||
$r = $this->prepare['selectOneEntity']->fetchAll(\PDO::FETCH_NUM);
|
||||
$this->prepare['selectOneEntity']->closeCursor();
|
||||
if (!empty($r)) {
|
||||
return $r[0][1];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectRangeEntities(int $key, int $limit): array
|
||||
{
|
||||
$this->prepare['selectRangeEntities']->bindParam(':startKey', $key, \PDO::PARAM_INT);
|
||||
$this->prepare['selectRangeEntities']->bindParam(':limit', $limit, \PDO::PARAM_INT);
|
||||
$this->prepare['selectRangeEntities']->execute();
|
||||
|
||||
$r = $this->prepare['selectRangeEntities']->fetchAll(\PDO::FETCH_NUM);
|
||||
$this->prepare['selectRangeEntities']->closeCursor();
|
||||
if (!empty($r)) {
|
||||
$result = [];
|
||||
foreach ($r as $value) {
|
||||
$result[(int) $value[0]] = $value[1];
|
||||
}
|
||||
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
271
include/condorcet/src/DataManager/VotesManager.php
Normal file
271
include/condorcet/src/DataManager/VotesManager.php
Normal file
@ -0,0 +1,271 @@
|
||||
<?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\DataManager;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Vote};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Throws};
|
||||
use CondorcetPHP\Condorcet\ElectionProcess\ElectionState;
|
||||
use CondorcetPHP\Condorcet\Throwable\VoteManagerException;
|
||||
use CondorcetPHP\Condorcet\Tools\Converters\CondorcetElectionFormat;
|
||||
|
||||
class VotesManager extends ArrayManager
|
||||
{
|
||||
/////////// Data CallBack for external drivers ///////////
|
||||
|
||||
protected function decodeOneEntity(string $data): Vote
|
||||
{
|
||||
$vote = new Vote($data);
|
||||
$vote->registerLink($this->getElection());
|
||||
$vote->notUpdate = true;
|
||||
$this->getElection()->checkVoteCandidate($vote);
|
||||
$vote->notUpdate = false;
|
||||
|
||||
return $vote;
|
||||
}
|
||||
|
||||
protected function encodeOneEntity(Vote $data): string
|
||||
{
|
||||
$data->destroyLink($this->getElection());
|
||||
|
||||
return str_replace([' > ', ' = '], ['>', '='], (string) $data);
|
||||
}
|
||||
|
||||
protected function preDeletedTask(Vote $object): void
|
||||
{
|
||||
$object->destroyLink($this->getElection());
|
||||
}
|
||||
|
||||
/////////// Array Access - Specials improvements ///////////
|
||||
|
||||
public function offsetGet(mixed $offset): Vote
|
||||
{
|
||||
return parent::offsetGet($offset);
|
||||
}
|
||||
|
||||
#[Throws(VoteManagerException::class)]
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if ($value instanceof Vote) {
|
||||
parent::offsetSet((\is_int($offset) ? $offset : null), $value);
|
||||
$this->UpdateAndResetComputing(key: $this->maxKey, type: 1);
|
||||
} else {
|
||||
throw new VoteManagerException;
|
||||
}
|
||||
|
||||
$this->checkRegularize();
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
$this->UpdateAndResetComputing(key: $offset, type: 2);
|
||||
parent::offsetUnset($offset);
|
||||
}
|
||||
|
||||
/////////// Internal Election related methods ///////////
|
||||
|
||||
public function UpdateAndResetComputing(int $key, int $type): void
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
|
||||
if ($election->getState() === ElectionState::VOTES_REGISTRATION) {
|
||||
if ($type === 1) {
|
||||
$election->getPairwise()->addNewVote($key);
|
||||
} elseif ($type === 2) {
|
||||
$election->getPairwise()->removeVote($key);
|
||||
}
|
||||
|
||||
$election->cleanupCalculator();
|
||||
} else {
|
||||
$election->setStateToVote();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// Get Votes Methods ///////////
|
||||
|
||||
public function getVoteKey(Vote $vote): ?int
|
||||
{
|
||||
($r = array_search(needle: $vote, haystack: $this->Container, strict: true)) !== false || ($r = array_search(needle: $vote, haystack: $this->Cache, strict: true));
|
||||
|
||||
return ($r !== false) ? $r : null;
|
||||
}
|
||||
|
||||
protected function getFullVotesListGenerator(): \Generator
|
||||
{
|
||||
foreach ($this as $voteKey => $vote) {
|
||||
yield $voteKey => $vote;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getPartialVotesListGenerator(array $tags, bool $with): \Generator
|
||||
{
|
||||
foreach ($this as $voteKey => $vote) {
|
||||
$noOne = true;
|
||||
foreach ($tags as $oneTag) {
|
||||
if (($oneTag === $voteKey) || \in_array(needle: $oneTag, haystack: $vote->getTags(), strict: true)) {
|
||||
if ($with) {
|
||||
yield $voteKey => $vote;
|
||||
break;
|
||||
} else {
|
||||
$noOne = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$with && $noOne) {
|
||||
yield $voteKey => $vote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the votes list
|
||||
public function getVotesList(?array $tags = null, bool $with = true): array
|
||||
{
|
||||
if ($tags === null) {
|
||||
return $this->getFullDataSet();
|
||||
} else {
|
||||
$search = [];
|
||||
|
||||
foreach ($this->getPartialVotesListGenerator($tags, $with) as $voteKey => $vote) {
|
||||
$search[$voteKey] = $vote;
|
||||
}
|
||||
|
||||
return $search;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the votes list as a generator object
|
||||
public function getVotesListGenerator(?array $tags = null, bool $with = true): \Generator
|
||||
{
|
||||
if ($tags === null) {
|
||||
return $this->getFullVotesListGenerator();
|
||||
} else {
|
||||
return $this->getPartialVotesListGenerator($tags, $with);
|
||||
}
|
||||
}
|
||||
|
||||
public function getVotesValidUnderConstraintGenerator(?array $tags = null, bool $with = true): \Generator
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$generator = ($tags === null) ? $this->getFullVotesListGenerator() : $this->getPartialVotesListGenerator($tags, $with);
|
||||
|
||||
foreach ($generator as $voteKey => $oneVote) {
|
||||
if (!$election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $voteKey => $oneVote;
|
||||
}
|
||||
}
|
||||
|
||||
public function getVotesListAsString(bool $withContext): string
|
||||
{
|
||||
$election = $this->getElection();
|
||||
|
||||
$simpleList = '';
|
||||
|
||||
$weight = [];
|
||||
$nb = [];
|
||||
|
||||
foreach ($this as $oneVote) {
|
||||
$oneVoteString = $oneVote->getSimpleRanking($withContext ? $election : null);
|
||||
|
||||
if (!\array_key_exists(key: $oneVoteString, array: $weight)) {
|
||||
$weight[$oneVoteString] = 0;
|
||||
}
|
||||
if (!\array_key_exists(key: $oneVoteString, array: $nb)) {
|
||||
$nb[$oneVoteString] = 0;
|
||||
}
|
||||
|
||||
if ($election->isVoteWeightAllowed()) {
|
||||
$weight[$oneVoteString] += $oneVote->getWeight();
|
||||
} else {
|
||||
$weight[$oneVoteString]++;
|
||||
}
|
||||
|
||||
$nb[$oneVoteString]++;
|
||||
}
|
||||
|
||||
ksort($weight, \SORT_NATURAL);
|
||||
arsort($weight);
|
||||
|
||||
$isFirst = true;
|
||||
foreach ($weight as $key => $value) {
|
||||
if (!$isFirst) {
|
||||
$simpleList .= "\n";
|
||||
}
|
||||
$voteString = ($key === '') ? CondorcetElectionFormat::SPECIAL_KEYWORD_EMPTY_RANKING : $key;
|
||||
$simpleList .= $voteString.' * '.$nb[$key];
|
||||
$isFirst = false;
|
||||
}
|
||||
|
||||
return $simpleList;
|
||||
}
|
||||
|
||||
public function countVotes(?array $tag, bool $with): int
|
||||
{
|
||||
if ($tag === null) {
|
||||
return \count($this);
|
||||
} else {
|
||||
$count = 0;
|
||||
|
||||
foreach ($this as $key => $value) {
|
||||
$noOne = true;
|
||||
foreach ($tag as $oneTag) {
|
||||
if (($oneTag === $key) || \in_array(needle: $oneTag, haystack: $value->getTags(), strict: true)) {
|
||||
if ($with) {
|
||||
$count++;
|
||||
break;
|
||||
} else {
|
||||
$noOne = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$with && $noOne) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
public function countInvalidVoteWithConstraints(): int
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$count = 0;
|
||||
|
||||
foreach ($this as $oneVote) {
|
||||
if (!$election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function sumVotesWeight(bool $constraint = false): int
|
||||
{
|
||||
$election = $this->getElection();
|
||||
$sum = 0;
|
||||
|
||||
foreach ($this as $oneVote) {
|
||||
if (!$constraint || $election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
|
||||
$sum += $election->isVoteWeightAllowed() ? $oneVote->getWeight() : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
482
include/condorcet/src/Election.php
Normal file
482
include/condorcet/src/Election.php
Normal file
@ -0,0 +1,482 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Throwable\{DataHandlerException, ElectionObjectVersionMismatchException, NoCandidatesException, NoSeatsException, ResultRequestedWithoutVotesException, VoteConstraintException};
|
||||
use CondorcetPHP\Condorcet\ElectionProcess\{CandidatesProcess, ElectionState, ResultsProcess, VotesProcess};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\DataManager\VotesManager;
|
||||
use CondorcetPHP\Condorcet\DataManager\DataHandlerDrivers\DataHandlerDriverInterface;
|
||||
use CondorcetPHP\Condorcet\Timer\Manager as Timer_Manager;
|
||||
|
||||
// Base Condorcet class
|
||||
class Election
|
||||
{
|
||||
/////////// CONSTRUCTOR ///////////
|
||||
|
||||
use CondorcetVersion;
|
||||
|
||||
|
||||
/////////// CANDIDATES ///////////
|
||||
|
||||
use CandidatesProcess;
|
||||
|
||||
|
||||
/////////// VOTING ///////////
|
||||
|
||||
use VotesProcess;
|
||||
|
||||
|
||||
/////////// RESULTS ///////////
|
||||
|
||||
use ResultsProcess;
|
||||
|
||||
/////////// PROPERTIES ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
public const MAX_CANDIDATE_NAME_LENGTH = 100; // Max length for candidate name string (UTF-8)
|
||||
|
||||
protected static ?int $maxParseIteration = null;
|
||||
protected static ?int $maxVoteNumber = null;
|
||||
protected static bool $checksumMode = false;
|
||||
|
||||
/////////// STATICS METHODS ///////////
|
||||
|
||||
// Change max parse iteration
|
||||
#[PublicAPI]
|
||||
#[Description('Maximum input for each use of Election::parseCandidate && Election::parseVote. Will throw an exception if exceeded.')]
|
||||
#[FunctionReturn('*(int or null)* The new limit.')]
|
||||
#[Related('static Election::setMaxVoteNumber')]
|
||||
public static function setMaxParseIteration(
|
||||
#[FunctionParameter('Null will deactivate this functionality. Else, enter an integer.')]
|
||||
?int $maxParseIterations
|
||||
): ?int {
|
||||
self::$maxParseIteration = $maxParseIterations;
|
||||
return self::$maxParseIteration;
|
||||
}
|
||||
|
||||
// Change max vote number
|
||||
#[PublicAPI]
|
||||
#[Description("Add a limitation on Election::addVote and related methods. You can't add new vote y the number of registered vote is equall ou superior of this limit.")]
|
||||
#[FunctionReturn('*(int or null)* The new limit.')]
|
||||
#[Related('static Election::setMaxParseIteration')]
|
||||
public static function setMaxVoteNumber(
|
||||
#[FunctionParameter('Null will deactivate this functionality. An integer will fix the limit.')]
|
||||
?int $maxVotesNumber
|
||||
): ?int {
|
||||
self::$maxVoteNumber = $maxVotesNumber;
|
||||
return self::$maxVoteNumber;
|
||||
}
|
||||
|
||||
// Mechanics
|
||||
protected ElectionState $State = ElectionState::CANDIDATES_REGISTRATION;
|
||||
protected Timer_Manager $timer;
|
||||
|
||||
// Params
|
||||
protected bool $ImplicitRanking = true;
|
||||
protected bool $VoteWeightRule = false;
|
||||
protected array $Constraints = [];
|
||||
protected int $Seats = 100;
|
||||
|
||||
// -------
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Build a new Election.')]
|
||||
public function __construct()
|
||||
{
|
||||
$this->Candidates = [];
|
||||
$this->Votes = new VotesManager($this);
|
||||
$this->timer = new Timer_Manager;
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
// Don't include others data
|
||||
$include = [
|
||||
'Candidates' => $this->Candidates,
|
||||
'Votes' => $this->Votes,
|
||||
|
||||
'State' => $this->State,
|
||||
'objectVersion' => $this->objectVersion,
|
||||
'AutomaticNewCandidateName' => $this->AutomaticNewCandidateName,
|
||||
|
||||
'ImplicitRanking' => $this->ImplicitRanking,
|
||||
'VoteWeightRule' => $this->VoteWeightRule,
|
||||
'Constraints' => $this->Constraints,
|
||||
|
||||
'Pairwise' => $this->Pairwise,
|
||||
'Calculator' => $this->Calculator,
|
||||
];
|
||||
|
||||
!self::$checksumMode && ($include += ['timer' => $this->timer]);
|
||||
|
||||
return $include;
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
// Only compare major and minor version numbers, not patch level
|
||||
// e.g. 2.0 and 3.2
|
||||
$objectVersion = explode('.', $data['objectVersion']);
|
||||
$objectVersion = $objectVersion[0] . '.' . $objectVersion[1];
|
||||
if (version_compare($objectVersion, Condorcet::getVersion(true), '!=')) {
|
||||
throw new ElectionObjectVersionMismatchException($objectVersion);
|
||||
}
|
||||
|
||||
$this->Candidates = $data['Candidates'];
|
||||
$this->Votes = $data['Votes'];
|
||||
$this->Votes->setElection($this);
|
||||
$this->registerAllLinks();
|
||||
|
||||
$this->AutomaticNewCandidateName = $data['AutomaticNewCandidateName'];
|
||||
$this->State = $data['State'];
|
||||
$this->objectVersion = $data['objectVersion'];
|
||||
|
||||
$this->ImplicitRanking = $data['ImplicitRanking'];
|
||||
$this->VoteWeightRule = $data['VoteWeightRule'];
|
||||
$this->Constraints = $data['Constraints'];
|
||||
|
||||
$this->Pairwise = $data['Pairwise'];
|
||||
$this->Pairwise->setElection($this);
|
||||
|
||||
$this->Calculator = $data['Calculator'];
|
||||
foreach ($this->Calculator as $methodObject) {
|
||||
$methodObject->setElection($this);
|
||||
}
|
||||
|
||||
$this->timer ??= $data['timer'];
|
||||
}
|
||||
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->Votes = clone $this->Votes;
|
||||
$this->Votes->setElection($this);
|
||||
$this->registerAllLinks();
|
||||
|
||||
$this->timer = clone $this->timer;
|
||||
|
||||
if ($this->Pairwise !== null) {
|
||||
$this->Pairwise = clone $this->Pairwise;
|
||||
$this->Pairwise->setElection($this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// TIMER & CHECKSUM ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Returns the cumulated computation runtime of this object. Include only computation related methods.')]
|
||||
#[FunctionReturn('(Float) Timer')]
|
||||
#[Example('Manual - Timber benchmarking', 'https://github.com/julien-boudry/Condorcet/wiki/III-%23-A.-Avanced-features---Configuration-%23-1.-Timer-Benchmarking')]
|
||||
#[Related('Election::getLastTimer')]
|
||||
public function getGlobalTimer(): float
|
||||
{
|
||||
return $this->timer->getGlobalTimer();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the last computation runtime (typically after a getResult() call.). Include only computation related methods.')]
|
||||
#[FunctionReturn('(Float) Timer')]
|
||||
#[Example('Manual - Timber benchmarking', 'https://github.com/julien-boudry/Condorcet/wiki/III-%23-A.-Avanced-features---Configuration-%23-1.-Timer-Benchmarking')]
|
||||
#[Related('Election::getGlobalTimer')]
|
||||
public function getLastTimer(): float
|
||||
{
|
||||
return $this->timer->getLastTimer();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the Timer manager object.')]
|
||||
#[FunctionReturn("An CondorcetPHP\Condorcet\Timer\Manager object using by this election.")]
|
||||
#[Related('Election::getGlobalTimer', 'Election::getLastTimer')]
|
||||
public function getTimerManager(): Timer_Manager
|
||||
{
|
||||
return $this->timer;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("SHA-2 256 checksum of following internal data:\n* Candidates\n* Votes list & tags\n* Computed data (pairwise, algorithm cache, stats)\n* Class version (major version like 0.14)\n\nCan be powerfull to check integrity and security of an election. Or working with serialized object.")]
|
||||
#[FunctionReturn('SHA-2 256 bits Hexadecimal')]
|
||||
#[Example('Manual - Cryptographic Checksum', 'https://github.com/julien-boudry/Condorcet/wiki/III-%23-A.-Avanced-features---Configuration-%23-2.-Cryptographic-Checksum')]
|
||||
public function getChecksum(): string
|
||||
{
|
||||
self::$checksumMode = true;
|
||||
|
||||
$r = hash_init('sha256');
|
||||
|
||||
foreach ($this->Candidates as $value) {
|
||||
hash_update($r, (string) $value);
|
||||
}
|
||||
|
||||
foreach ($this->Votes as $value) {
|
||||
hash_update($r, (string) $value);
|
||||
}
|
||||
|
||||
$this->Pairwise !== null
|
||||
&& hash_update($r, serialize($this->Pairwise->getExplicitPairwise()));
|
||||
|
||||
hash_update($r, $this->getObjectVersion(true));
|
||||
|
||||
self::$checksumMode = false;
|
||||
|
||||
return hash_final($r);
|
||||
}
|
||||
|
||||
|
||||
/////////// LINKS REGULATION ///////////
|
||||
|
||||
protected function registerAllLinks(): void
|
||||
{
|
||||
foreach ($this->Candidates as $value) {
|
||||
$value->registerLink($this);
|
||||
}
|
||||
|
||||
foreach ($this->Votes as $value) {
|
||||
$value->registerLink($this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// IMPLICIT RANKING & VOTE WEIGHT ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Returns the corresponding setting as currently set (True by default).\nIf it is True then all votes expressing a partial ranking are understood as implicitly placing all the non-mentioned candidates exequos on a last rank.\nIf it is false, then the candidates not ranked, are not taken into account at all.")]
|
||||
#[FunctionReturn('True / False')]
|
||||
#[Related('Election::setImplicitRanking')]
|
||||
public function getImplicitRankingRule(): bool
|
||||
{
|
||||
return $this->ImplicitRanking;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Set the setting and reset all result data.\nIf it is True then all votes expressing a partial ranking are understood as implicitly placing all the non-mentioned candidates exequos on a last rank.\nIf it is false, then the candidates not ranked, are not taken into account at all.")]
|
||||
#[FunctionReturn('Return True')]
|
||||
#[Related('Election::getImplicitRankingRule')]
|
||||
public function setImplicitRanking(
|
||||
#[FunctionParameter('New rule')]
|
||||
bool $rule = true
|
||||
): bool {
|
||||
$this->ImplicitRanking = $rule;
|
||||
$this->cleanupCompute();
|
||||
return $this->getImplicitRankingRule();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Returns the corresponding setting as currently set (False by default).\nIf it is True then votes vote optionally can use weight otherwise (if false) all votes will be evaluated as equal for this election.")]
|
||||
#[FunctionReturn('True / False')]
|
||||
#[Related('Election::allowsVoteWeight')]
|
||||
public function isVoteWeightAllowed(): bool
|
||||
{
|
||||
return $this->VoteWeightRule;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Set the setting and reset all result data.\nThen the weight of votes (if specified) will be taken into account when calculating the results. Otherwise all votes will be considered equal.\nBy default, the voting weight is not activated and all votes are considered equal.")]
|
||||
#[FunctionReturn('Return True')]
|
||||
#[Related('Election::isVoteWeightAllowed')]
|
||||
public function allowsVoteWeight(
|
||||
#[FunctionParameter('New rule')]
|
||||
bool $rule = true
|
||||
): bool {
|
||||
$this->VoteWeightRule = $rule;
|
||||
$this->cleanupCompute();
|
||||
return $this->isVoteWeightAllowed();
|
||||
}
|
||||
|
||||
|
||||
/////////// VOTE CONSTRAINT ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Add a constraint rules as a valid class path.')]
|
||||
#[FunctionReturn('True on success.')]
|
||||
#[Throws(VoteConstraintException::class)]
|
||||
#[Example('Manual - Vote Constraints', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-5.-Vote-Constraints')]
|
||||
#[Related('Election::getConstraints', 'Election::clearConstraints', 'Election::testIfVoteIsValidUnderElectionConstraints')]
|
||||
public function addConstraint(
|
||||
#[FunctionParameter('A valid class path. Class must extend VoteConstraint class')]
|
||||
string $constraintClass
|
||||
): bool {
|
||||
if (!class_exists($constraintClass)) {
|
||||
throw new VoteConstraintException('class is not defined');
|
||||
} elseif (!is_subclass_of($constraintClass, VoteConstraintInterface::class)) {
|
||||
throw new VoteConstraintException('class is not a valid subclass');
|
||||
} elseif (\in_array(needle: $constraintClass, haystack: $this->getConstraints(), strict: true)) {
|
||||
throw new VoteConstraintException('class is already registered');
|
||||
}
|
||||
|
||||
$this->cleanupCompute();
|
||||
|
||||
|
||||
$this->Constraints[] = $constraintClass;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get active constraints list.')]
|
||||
#[FunctionReturn('Array with class name of each active constraint. Empty array if there is not.')]
|
||||
#[Example('Manual - Vote Constraints', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-5.-Vote-Constraints')]
|
||||
#[Related('Election::clearConstraints', 'Election::addConstraints', 'Election::testIfVoteIsValidUnderElectionConstraints')]
|
||||
public function getConstraints(): array
|
||||
{
|
||||
return $this->Constraints;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Clear all constraints rules and clear previous results.')]
|
||||
#[FunctionReturn('Return True.')]
|
||||
#[Example('Manual - Vote Constraints', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-5.-Vote-Constraints')]
|
||||
#[Related('Election::getConstraints', 'Election::addConstraints', 'Election::testIfVoteIsValidUnderElectionConstraints')]
|
||||
public function clearConstraints(): bool
|
||||
{
|
||||
$this->Constraints = [];
|
||||
|
||||
$this->cleanupCompute();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Test if a vote is valid with these election constraints.')]
|
||||
#[FunctionReturn('Return True if vote will pass the constraints rules, else False.')]
|
||||
#[Example('Manual - Vote Constraints', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-5.-Vote-Constraints')]
|
||||
#[Related('Election::getConstraints', 'Election::addConstraints', 'Election::clearConstraints')]
|
||||
public function testIfVoteIsValidUnderElectionConstraints(
|
||||
#[FunctionParameter('A vote. Not necessarily registered in this election')]
|
||||
Vote $vote
|
||||
): bool {
|
||||
foreach ($this->Constraints as $oneConstraint) {
|
||||
if ($oneConstraint::isVoteAllow($this, $vote) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/////////// STV SEATS ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get number of Seats for STV methods.')]
|
||||
#[FunctionReturn('Number of seats.')]
|
||||
#[Related('Election::setNumberOfSeats', 'Result::getNumberOfSeats')]
|
||||
public function getNumberOfSeats(): int
|
||||
{
|
||||
return $this->Seats;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Set number of Seats for STV methods.')]
|
||||
#[FunctionReturn('Number of seats.')]
|
||||
#[Throws(NoSeatsException::class)]
|
||||
#[Related('Election::getNumberOfSeats')]
|
||||
public function setNumberOfSeats(
|
||||
#[FunctionParameter('The number of seats for proportional methods.')]
|
||||
int $seats
|
||||
): int {
|
||||
if ($seats > 0) {
|
||||
$this->cleanupCompute();
|
||||
|
||||
$this->Seats = $seats;
|
||||
} else {
|
||||
throw new NoSeatsException;
|
||||
}
|
||||
|
||||
return $this->Seats;
|
||||
}
|
||||
|
||||
|
||||
/////////// LARGE ELECTION MODE ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Import and enable an external driver to store vote on very large election.')]
|
||||
#[FunctionReturn('True if success. Else throw an Exception.')]
|
||||
#[Throws(DataHandlerException::class)]
|
||||
#[Example('[Manual - DataHandler]', 'https://github.com/julien-boudry/Condorcet/blob/master/examples/specifics_examples/use_large_election_external_database_drivers.php')]
|
||||
#[Related('Election::removeExternalDataHandler')]
|
||||
public function setExternalDataHandler(
|
||||
#[FunctionParameter('Driver object')]
|
||||
DataHandlerDriverInterface $driver
|
||||
): bool {
|
||||
if (!$this->Votes->isUsingHandler()) {
|
||||
$this->Votes->importHandler($driver);
|
||||
return true;
|
||||
} else {
|
||||
throw new DataHandlerException('external data handler cannot be imported');
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Remove an external driver to store vote on very large election. And import his data into classical memory.')]
|
||||
#[FunctionReturn('True if success. Else throw an Exception.')]
|
||||
#[Throws(DataHandlerException::class)]
|
||||
#[Related('Election::setExternalDataHandler')]
|
||||
public function removeExternalDataHandler(): bool
|
||||
{
|
||||
if ($this->Votes->isUsingHandler()) {
|
||||
$this->Votes->closeHandler();
|
||||
return true;
|
||||
} else {
|
||||
throw new DataHandlerException('external data handler cannot be removed, is already in use');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////// STATE ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the election process level.')]
|
||||
#[FunctionReturn("ElectionState::CANDIDATES_REGISTRATION: Candidate registered state. No votes, no result, no cache.\nElectionState::VOTES_REGISTRATION: Voting registration phase. Pairwise cache can exist thanks to dynamic computation if voting phase continue after the first get result. But method result never exist.\n3: Result phase: Some method result may exist, pairwise exist. An election will return to Phase 2 if votes are added or modified dynamically.")]
|
||||
#[Related('Election::setStateToVote')]
|
||||
public function getState(): ElectionState
|
||||
{
|
||||
return $this->State;
|
||||
}
|
||||
|
||||
// Close the candidate config, be ready for voting (optional)
|
||||
#[PublicAPI]
|
||||
#[Description("Force the election to get back to state 2. See Election::getState.\nIt is not necessary to use this method. The election knows how to manage its phase changes on its own. But it is a way to clear the cache containing the results of the methods.\n\nIf you are on state 1 (candidate registering), it's will close this state and prepare election to get firsts votes.\nIf you are on state 3. The method result cache will be clear, but not the pairwise. Which will continue to be updated dynamically.")]
|
||||
#[FunctionReturn('Always True.')]
|
||||
#[Throws(NoCandidatesException::class, ResultRequestedWithoutVotesException::class)]
|
||||
#[Related('Election::getState')]
|
||||
public function setStateToVote(): bool
|
||||
{
|
||||
if ($this->State === ElectionState::CANDIDATES_REGISTRATION) {
|
||||
if (empty($this->Candidates)) {
|
||||
throw new NoCandidatesException;
|
||||
}
|
||||
|
||||
$this->State = ElectionState::VOTES_REGISTRATION;
|
||||
$this->preparePairwiseAndCleanCompute();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prepare to compute results & caching system
|
||||
protected function preparePairwiseAndCleanCompute(): bool
|
||||
{
|
||||
if ($this->Pairwise === null && $this->State === ElectionState::VOTES_REGISTRATION) {
|
||||
$this->cleanupCompute();
|
||||
|
||||
// Do Pairwise
|
||||
$this->makePairwise();
|
||||
|
||||
// Return
|
||||
return true;
|
||||
} elseif ($this->State === ElectionState::CANDIDATES_REGISTRATION || $this->countVotes() === 0) {
|
||||
throw new ResultRequestedWithoutVotesException;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
282
include/condorcet/src/ElectionProcess/CandidatesProcess.php
Normal file
282
include/condorcet/src/ElectionProcess/CandidatesProcess.php
Normal file
@ -0,0 +1,282 @@
|
||||
<?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\ElectionProcess;
|
||||
|
||||
use CondorcetPHP\Condorcet\Candidate;
|
||||
use CondorcetPHP\Condorcet\Throwable\{CandidateDoesNotExistException, CandidateExistsException, VoteMaxNumberReachedException, VotingHasStartedException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, InternalModulesAPI, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\Utils\CondorcetUtil;
|
||||
|
||||
// Manage Candidates for Election class
|
||||
trait CandidatesProcess
|
||||
{
|
||||
/////////// CONSTRUCTOR ///////////
|
||||
|
||||
// Data and global options
|
||||
protected array $Candidates = []; // Candidate list
|
||||
protected string $AutomaticNewCandidateName = 'A';
|
||||
|
||||
|
||||
/////////// GET CANDIDATES ///////////
|
||||
|
||||
// Count registered candidates
|
||||
#[PublicAPI]
|
||||
#[Description('Count the number of registered candidates')]
|
||||
#[FunctionReturn('Number of registered candidates for this election.')]
|
||||
#[Related('Election::getCandidatesList')]
|
||||
public function countCandidates(): int
|
||||
{
|
||||
return \count($this->Candidates);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return a list of registered candidates for this election.')]
|
||||
#[FunctionReturn('List of candidates in an array.')]
|
||||
#[Related('Election::countCandidates')]
|
||||
public function getCandidatesList(): array
|
||||
{
|
||||
return $this->Candidates;
|
||||
}
|
||||
|
||||
// Get the list of registered CANDIDATES
|
||||
#[PublicAPI]
|
||||
#[Description('Return a list of registered candidates for this election.')]
|
||||
#[FunctionReturn('List of candidates in an array populated with strings instead of CandidateObjects.')]
|
||||
#[Related('Election::countCandidates')]
|
||||
public function getCandidatesListAsString(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->Candidates as $candidateKey => &$oneCandidate) {
|
||||
$result[$candidateKey] = $oneCandidate->getName();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getCandidateKey(Candidate|string $candidate): ?int
|
||||
{
|
||||
if ($candidate instanceof Candidate) {
|
||||
$r = array_search(needle: $candidate, haystack: $this->Candidates, strict: true);
|
||||
} else {
|
||||
$r = array_search(needle: trim((string) $candidate), haystack: $this->Candidates, strict: false);
|
||||
}
|
||||
|
||||
return ($r !== false) ? $r : null;
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getCandidateObjectFromKey(int $candidate_key): ?Candidate
|
||||
{
|
||||
return $this->Candidates[$candidate_key] ?? null;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Check if a candidate is already taking part in the election.')]
|
||||
#[FunctionReturn('True / False')]
|
||||
#[Related('Election::addCandidate')]
|
||||
public function isRegisteredCandidate(
|
||||
#[FunctionParameter('Candidate object or candidate string name. String name works only if the strict mode is active')]
|
||||
Candidate|string $candidate,
|
||||
#[FunctionParameter("Search comparison mode. In strict mode, candidate objects are compared strictly and a string input can't match anything.\nIf strict mode is false, the comparison will be based on name")]
|
||||
bool $strictMode = true
|
||||
): bool {
|
||||
return $strictMode ? \in_array(needle: $candidate, haystack: $this->Candidates, strict: true) : \in_array(needle: (string) $candidate, haystack: $this->Candidates, strict: false);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Find candidate object by string and return the candidate object.')]
|
||||
#[FunctionReturn('Candidate object')]
|
||||
public function getCandidateObjectFromName(
|
||||
#[FunctionParameter('Candidate name')]
|
||||
string $candidateName
|
||||
): ?Candidate {
|
||||
foreach ($this->Candidates as $oneCandidate) {
|
||||
if ($oneCandidate->getName() === $candidateName) {
|
||||
return $oneCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/////////// ADD & REMOVE CANDIDATE ///////////
|
||||
|
||||
// Add a vote candidate before voting
|
||||
#[PublicAPI]
|
||||
#[Description('Add one candidate to an election.')]
|
||||
#[FunctionReturn('The new candidate object (your or automatic one). Throws an exception on error (existing candidate...).')]
|
||||
#[Throws(CandidateExistsException::class, VotingHasStartedException::class)]
|
||||
#[Example('Manual - Manage Candidate', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-A.-Create-an-Election-%23-2.-Create-Candidates')]
|
||||
#[Related('Election::parseCandidates', 'Election::addCandidatesFromJson', 'Election::removeCandidate', 'Election::getCandidatesList', 'Election::canAddCandidate')]
|
||||
public function addCandidate(
|
||||
#[FunctionParameter('Alphanumeric string or CondorcetPHP\Condorcet\Candidate object. The whitespace of your candidate name will be trimmed. If null, this function will create a new candidate with an automatic name.')]
|
||||
Candidate|string|null $candidate = null
|
||||
): Candidate {
|
||||
// only if the vote has not started
|
||||
if ($this->State->value > ElectionState::CANDIDATES_REGISTRATION->value) {
|
||||
throw new VotingHasStartedException("cannot add '{$candidate}'");
|
||||
}
|
||||
|
||||
// Process
|
||||
if (empty($candidate) && $candidate !== '0') {
|
||||
while (!$this->canAddCandidate($this->AutomaticNewCandidateName)) {
|
||||
$this->AutomaticNewCandidateName++;
|
||||
}
|
||||
|
||||
$newCandidate = new Candidate($this->AutomaticNewCandidateName);
|
||||
} else { // Try to add the candidate_id
|
||||
$newCandidate = ($candidate instanceof Candidate) ? $candidate : new Candidate((string) $candidate);
|
||||
|
||||
if (!$this->canAddCandidate($newCandidate)) {
|
||||
throw new CandidateExistsException((string) $candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// Register it
|
||||
$this->Candidates[] = $newCandidate;
|
||||
|
||||
// Linking
|
||||
$newCandidate->registerLink($this);
|
||||
|
||||
// Disallow other candidate object name matching
|
||||
$newCandidate->setProvisionalState(false);
|
||||
|
||||
return $newCandidate;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Check if a candidate is already registered. Uses strict Vote object comparison, but also string naming comparison in the election.')]
|
||||
#[FunctionReturn('True if your candidate is available, false otherwise.')]
|
||||
#[Related('Election::addCandidate')]
|
||||
public function canAddCandidate(
|
||||
#[FunctionParameter('String or Condorcet/Vote object')]
|
||||
Candidate|string $candidate
|
||||
): bool {
|
||||
return !$this->isRegisteredCandidate($candidate, false);
|
||||
}
|
||||
|
||||
// Destroy a register vote candidate before voting
|
||||
#[PublicAPI]
|
||||
#[Description("Remove candidates from an election.\n\n*Please note: You can't remove candidates after the first vote. An exception will be thrown.*")]
|
||||
#[FunctionReturn("List of removed CondorcetPHP\Condorcet\Candidate object.")]
|
||||
#[Throws(CandidateDoesNotExistException::class, VotingHasStartedException::class)]
|
||||
#[Example('Manual - Manage Candidate', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-A.-Create-an-Election-%23-2.-Create-Candidates')]
|
||||
#[Related('Election::addCandidate', 'Election::getCandidatesList')]
|
||||
public function removeCandidates(
|
||||
#[FunctionParameter("* String matching candidate name\n* CondorcetPHP\Condorcet\Candidate object\n* Array populated by CondorcetPHP\Condorcet\Candidate\n* Array populated by string matching candidate name")]
|
||||
array|Candidate|string $candidates_input
|
||||
): array {
|
||||
// only if the vote has not started
|
||||
if ($this->State->value > ElectionState::CANDIDATES_REGISTRATION->value) {
|
||||
throw new VotingHasStartedException;
|
||||
}
|
||||
|
||||
if (!\is_array($candidates_input)) {
|
||||
$candidates_input = [$candidates_input];
|
||||
}
|
||||
|
||||
foreach ($candidates_input as &$candidate) {
|
||||
$candidate_key = $this->getCandidateKey($candidate);
|
||||
|
||||
if ($candidate_key === null) {
|
||||
throw new CandidateDoesNotExistException($candidate->getName());
|
||||
}
|
||||
|
||||
$candidate = $candidate_key;
|
||||
}
|
||||
|
||||
$rem = [];
|
||||
foreach ($candidates_input as $candidate_key) {
|
||||
$this->Candidates[$candidate_key]->destroyLink($this);
|
||||
|
||||
$rem[] = $this->Candidates[$candidate_key];
|
||||
|
||||
unset($this->Candidates[$candidate_key]);
|
||||
}
|
||||
|
||||
return $rem;
|
||||
}
|
||||
|
||||
|
||||
/////////// PARSE CANDIDATES ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Import candidate from a JSON source.')]
|
||||
#[FunctionReturn('List of newly registered candidate object.')]
|
||||
#[Throws(CandidateExistsException::class)]
|
||||
#[Example('Manual - Manage Candidates', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-A.-Create-an-Election-%23-2.-Create-Candidates')]
|
||||
#[Related('Election::addCandidate', 'Election::parseCandidates', 'Election::addVotesFromJson')]
|
||||
public function addCandidatesFromJson(
|
||||
#[FunctionParameter('JSON string input')]
|
||||
string $input
|
||||
): array {
|
||||
$input = CondorcetUtil::prepareJson($input);
|
||||
|
||||
// -------
|
||||
|
||||
$adding = [];
|
||||
foreach ($input as $candidate) {
|
||||
$candidate = new Candidate($candidate);
|
||||
|
||||
if (!$this->canAddCandidate($candidate)) {
|
||||
throw new CandidateExistsException((string) $candidate);
|
||||
}
|
||||
|
||||
$adding[] = $candidate;
|
||||
}
|
||||
|
||||
// Add Candidates
|
||||
foreach ($adding as $oneCandidate) {
|
||||
$this->addCandidate($oneCandidate);
|
||||
}
|
||||
|
||||
return $adding;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Import candidate from a text source.')]
|
||||
#[FunctionReturn('List of newly registered candidate object. Count it for checking if all candidates have been correctly registered.')]
|
||||
#[Throws(CandidateExistsException::class, VoteMaxNumberReachedException::class)]
|
||||
#[Example('Manual - Manage Candidates', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-A.-Create-an-Election-%23-2.-Create-Candidates')]
|
||||
#[Related('Election::addCandidate', 'Election::addCandidatesFromJson', 'Election::parseVotes')]
|
||||
public function parseCandidates(
|
||||
#[FunctionParameter('String or valid path to a text file')]
|
||||
string $input,
|
||||
#[FunctionParameter('If true, the input is evaluated as path to a text file')]
|
||||
bool $isFile = false
|
||||
): array {
|
||||
$input = CondorcetUtil::prepareParse($input, $isFile);
|
||||
|
||||
$adding = [];
|
||||
foreach ($input as $line) {
|
||||
// addCandidate
|
||||
if (self::$maxParseIteration !== null && \count($adding) >= self::$maxParseIteration) {
|
||||
throw new VoteMaxNumberReachedException(self::$maxParseIteration);
|
||||
}
|
||||
|
||||
if (!$this->canAddCandidate($line)) {
|
||||
throw new CandidateExistsException($line);
|
||||
}
|
||||
|
||||
$adding[] = $line;
|
||||
}
|
||||
|
||||
foreach ($adding as $oneNewCandidate) {
|
||||
$this->addCandidate($oneNewCandidate);
|
||||
}
|
||||
|
||||
return $adding;
|
||||
}
|
||||
}
|
19
include/condorcet/src/ElectionProcess/ElectionState.php
Normal file
19
include/condorcet/src/ElectionProcess/ElectionState.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?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\ElectionProcess;
|
||||
|
||||
// Manage Candidates for Election class
|
||||
enum ElectionState: int
|
||||
{
|
||||
case CANDIDATES_REGISTRATION = 1;
|
||||
case VOTES_REGISTRATION = 2;
|
||||
}
|
294
include/condorcet/src/ElectionProcess/ResultsProcess.php
Normal file
294
include/condorcet/src/ElectionProcess/ResultsProcess.php
Normal file
@ -0,0 +1,294 @@
|
||||
<?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\ElectionProcess;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Candidate, Condorcet, Result};
|
||||
use CondorcetPHP\Condorcet\Algo\{Pairwise, StatsVerbosity};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\Throwable\AlgorithmException;
|
||||
use CondorcetPHP\Condorcet\Timer\Chrono as Timer_Chrono;
|
||||
|
||||
// Manage Results for Election class
|
||||
trait ResultsProcess
|
||||
{
|
||||
/////////// CONSTRUCTOR ///////////
|
||||
|
||||
// Result
|
||||
protected ?Pairwise $Pairwise = null;
|
||||
protected ?array $Calculator = null;
|
||||
protected StatsVerbosity $StatsVerbosity = StatsVerbosity::STD;
|
||||
|
||||
|
||||
/////////// GET RESULTS ///////////
|
||||
|
||||
// Generic function for default result with ability to change default object method
|
||||
#[PublicAPI]
|
||||
#[Description("Get a full ranking from an advanced Condorcet method.\n*Have a look on the [supported method](https://github.com/julien-boudry/Condorcet/wiki/I-%23-Installation-%26-Basic-Configuration-%23-2.-Condorcet-Methods), or create [your own algorithm](https://github.com/julien-boudry/Condorcet/wiki/III-%23-C.-Extending-Condorcet-%23-1.-Add-your-own-ranking-algorithm).*")]
|
||||
#[FunctionReturn('An Condorcet/Result Object (implementing ArrayAccess and Iterator, can be use like an array ordered by rank)')]
|
||||
#[Throws(AlgorithmException::class)]
|
||||
#[Example('Manual - Ranking from Condorcet Method', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-2.-Get-Ranking-from-Condorcet-advanced-Methods')]
|
||||
#[Related('Election::getWinner', 'Election::getResult', 'Condorcet::getDefaultMethod')]
|
||||
public function getResult(
|
||||
#[FunctionParameter('Not required for use election default method. Set the string name of the algorithm for use of a specific one.')]
|
||||
?string $method = null,
|
||||
#[FunctionParameter('Array of option for some methods. Look at each method documentation.')]
|
||||
array $methodOptions = []
|
||||
): Result {
|
||||
$methodOptions = self::formatResultOptions($methodOptions);
|
||||
|
||||
// Filter if tag is provided & return
|
||||
if ($methodOptions['%tagFilter']) {
|
||||
$chrono = (Condorcet::$UseTimer === true) ? new Timer_Chrono($this->timer, 'GetResult with filter') : null;
|
||||
|
||||
$filter = new self;
|
||||
|
||||
foreach ($this->getCandidatesList() as $candidate) {
|
||||
$filter->addCandidate($candidate);
|
||||
}
|
||||
|
||||
foreach ($this->getVotesList(tags: $methodOptions['tags'], with: $methodOptions['withTag']) as $vote) {
|
||||
$filter->addVote($vote);
|
||||
}
|
||||
|
||||
unset($chrono);
|
||||
|
||||
return $filter->getResult($method);
|
||||
}
|
||||
|
||||
////// Start //////
|
||||
|
||||
// Prepare
|
||||
$this->preparePairwiseAndCleanCompute();
|
||||
|
||||
// -------
|
||||
|
||||
$chrono = (Condorcet::$UseTimer === true) ? new Timer_Chrono($this->timer) : null;
|
||||
|
||||
if ($method === null) {
|
||||
$this->initResult(Condorcet::getDefaultMethod());
|
||||
$result = $this->Calculator[Condorcet::getDefaultMethod()]->getResult();
|
||||
} elseif ($wanted_method = Condorcet::getMethodClass($method)) {
|
||||
$this->initResult($wanted_method);
|
||||
$result = $this->Calculator[$wanted_method]->getResult();
|
||||
} else {
|
||||
throw new AlgorithmException($method);
|
||||
}
|
||||
|
||||
($chrono !== null) && $chrono->setRole('GetResult for '.$method);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the natural Condorcet winner if there is one. Alternatively you can get the winner(s) from an advanced Condorcet algorithm.')]
|
||||
#[FunctionReturn("Candidate object given. Null if there are no available winner or loser.\n\nIf you use an advanced method instead of Natural, you can get an array with multiples winners.\n\nThrow an exception on error.")]
|
||||
#[Example('Manual - Natural Condorcet', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-1.-Natural-Condorcet')]
|
||||
#[Related('Election::getCondorcetWinner', 'Election::getLoser', 'Election::getResult')]
|
||||
public function getWinner(
|
||||
#[FunctionParameter("*Only if not null: *\n\nThe winner will be provided by an advanced algorithm of an available advanced Condorcet method. For most of them, it will be the same as the Condorcet Marquis there. But if it does not exist, it may be different; and in some cases they may be multiple. \n\nIf null, Natural Condorcet algorithm will be use.")]
|
||||
?string $method = null
|
||||
): array|Candidate|null {
|
||||
$algo = Condorcet::condorcetBasicSubstitution($method);
|
||||
|
||||
// -------
|
||||
|
||||
if ($algo === Condorcet::CONDORCET_BASIC_CLASS) {
|
||||
(Condorcet::$UseTimer === true) && new Timer_Chrono($this->timer, 'GetWinner for CondorcetBasic');
|
||||
$this->initResult($algo);
|
||||
$result = $this->Calculator[$algo]->getWinner();
|
||||
|
||||
return ($result === null) ? null : $this->getCandidateObjectFromKey($result);
|
||||
} else {
|
||||
return $this->getResult($algo)->getWinner();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the natural Condorcet loser if there is one. Alternatively you can get the loser(s) from an advanced Condorcet algorithm.')]
|
||||
#[FunctionReturn("Candidate object given. Null if there are no available winner or loser.\n\nIf you use an advanced method instead of Natural, you can get an array with multiples losers.\n\nThrow an exception on error.")]
|
||||
#[Example('Manual - Natural Condorcet', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-1.-Natural-Condorcet')]
|
||||
#[Related('Election::getWinner', 'Election::getResult')]
|
||||
public function getLoser(
|
||||
#[FunctionParameter("*Only if not nulle:* \n\nThe loser will be provided by an advanced algorithm of an available advanced Condorcet method. For most of them, it will be the same as the Condorcet Marquis there. But if it does not exist, it may be different; and in some cases they may be multiple. \n\n If null, Natural Condorcet algorithm will be use.")]
|
||||
?string $method = null
|
||||
): array|Candidate|null {
|
||||
$algo = Condorcet::condorcetBasicSubstitution($method);
|
||||
|
||||
// -------
|
||||
|
||||
if ($algo === Condorcet::CONDORCET_BASIC_CLASS) {
|
||||
(Condorcet::$UseTimer === true) && new Timer_Chrono($this->timer, 'GetLoser for CondorcetBasic');
|
||||
$this->initResult($algo);
|
||||
$result = $this->Calculator[$algo]->getLoser();
|
||||
|
||||
return ($result === null) ? null : $this->getCandidateObjectFromKey($result);
|
||||
} else {
|
||||
return $this->getResult($algo)->getLoser();
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the natural Condorcet winner if there is one.')]
|
||||
#[FunctionReturn('Candidate object given. Null if there are no available winner.')]
|
||||
#[Example('Manual - Natural Condorcet', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-1.-Natural-Condorcet')]
|
||||
#[Related('Election::getCondorcetLoser', 'Election::getWiner', 'Election::getResult')]
|
||||
public function getCondorcetWinner(): ?Candidate
|
||||
{
|
||||
return $this->getWinner(null);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the natural Condorcet loser if there is one.')]
|
||||
#[FunctionReturn('Candidate object given. Null if there are no available loser.')]
|
||||
#[Example('Manual - Natural Condorcet', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-1.-Natural-Condorcet')]
|
||||
#[Related('Election::getCondorcetWinner', 'Election::getLoser', 'Election::getResult')]
|
||||
public function getCondorcetLoser(): ?Candidate
|
||||
{
|
||||
return $this->getLoser(null);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the Pairwise.')]
|
||||
#[FunctionReturn('Pairwise object.')]
|
||||
#[Example('Manual - Advanced Results', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-4.-Advanced-Results-Management')]
|
||||
#[Related('Election::getExplicitPairwise', 'Election::getResult')]
|
||||
public function getPairwise(): Pairwise
|
||||
{
|
||||
if ($this->Pairwise === null) {
|
||||
$this->preparePairwiseAndCleanCompute();
|
||||
}
|
||||
|
||||
return $this->Pairwise;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the Pairwise.')]
|
||||
#[FunctionReturn('Pairwise as an explicit array .')]
|
||||
#[Related('Election::getPairwise', 'Election::getResult')]
|
||||
public function getExplicitPairwise(): array
|
||||
{
|
||||
return $this->getPairwise()->getExplicitPairwise();
|
||||
}
|
||||
|
||||
// Generic function for default result with ability to change default object method
|
||||
#[PublicAPI]
|
||||
#[Description('Set an option to a method module and reset his cache for this election object. Be aware that this option applies to all election objects and remains in memory.')]
|
||||
#[FunctionReturn('True on success. Else False.')]
|
||||
#[Related('Result::getMethodOptions')]
|
||||
public function setMethodOption(
|
||||
#[FunctionParameter('Method name or class path')]
|
||||
string $method,
|
||||
#[FunctionParameter('Option name')]
|
||||
string $optionName,
|
||||
#[FunctionParameter('Option Value')]
|
||||
array|\BackedEnum|int|float|string $optionValue
|
||||
): bool {
|
||||
if ($method = Condorcet::getMethodClass($method)) {
|
||||
$method::setOption($optionName, $optionValue);
|
||||
unset($this->Calculator[$method]);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('The current level of stats verbosity for this election object. Look at Election->setStatsVerbosity method for more informations.')]
|
||||
#[FunctionReturn('The current verbosity level for this election object.')]
|
||||
public function getStatsVerbosity(): StatsVerbosity
|
||||
{
|
||||
return $this->StatsVerbosity;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Set a verbosity level for Result->statsVerbosity on returning Result objects. High level can slow down processing and use more memory (many more) than LOW and STD (default) level on somes methods.')]
|
||||
#[Related('Election::getVerbosity', 'Result::getVerbosity')]
|
||||
public function setStatsVerbosity(
|
||||
#[FunctionParameter('A verbosity level')]
|
||||
StatsVerbosity $StatsVerbosity
|
||||
): void {
|
||||
if ($StatsVerbosity !== $this->StatsVerbosity) {
|
||||
$this->cleanupCalculator();
|
||||
}
|
||||
|
||||
$this->StatsVerbosity = $StatsVerbosity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////// MAKE RESULTS ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Really similar to Election::getResult() but not return anything. Just calculates silently and fill the cache.')]
|
||||
#[Related('Election::getWinner', 'Election::getResult', 'Condorcet::getDefaultMethod')]
|
||||
public function computeResult(
|
||||
#[FunctionParameter('Not requiered for use object default method. Set the string name of the algorithm for use an specific one')]
|
||||
?string $method = null
|
||||
): void {
|
||||
$this->getResult($method);
|
||||
}
|
||||
|
||||
protected function makePairwise(): void
|
||||
{
|
||||
$this->cleanupCalculator();
|
||||
$this->Pairwise = new Pairwise($this);
|
||||
}
|
||||
|
||||
protected function initResult(string $class): void
|
||||
{
|
||||
if (!isset($this->Calculator[$class])) {
|
||||
$this->Calculator[$class] = new $class($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function debugGetCalculator(): ?array
|
||||
{
|
||||
return $this->Calculator;
|
||||
}
|
||||
|
||||
// Cleanup results to compute again with new votes
|
||||
protected function cleanupCompute(): void
|
||||
{
|
||||
// Algos
|
||||
$this->cleanupCalculator();
|
||||
|
||||
// Clean pairwise
|
||||
$this->Pairwise = null;
|
||||
}
|
||||
|
||||
public function cleanupCalculator(): void
|
||||
{
|
||||
$this->Calculator = null;
|
||||
}
|
||||
|
||||
|
||||
/////////// UTILS ///////////
|
||||
|
||||
protected static function formatResultOptions(array $arg): array
|
||||
{
|
||||
// About tag filter
|
||||
if (isset($arg['tags'])) {
|
||||
$arg['%tagFilter'] = true;
|
||||
|
||||
if (!isset($arg['withTag']) || !\is_bool($arg['withTag'])) {
|
||||
$arg['withTag'] = true;
|
||||
}
|
||||
} else {
|
||||
$arg['%tagFilter'] = false;
|
||||
}
|
||||
|
||||
return $arg;
|
||||
}
|
||||
}
|
519
include/condorcet/src/ElectionProcess/VotesProcess.php
Normal file
519
include/condorcet/src/ElectionProcess/VotesProcess.php
Normal file
@ -0,0 +1,519 @@
|
||||
<?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\ElectionProcess;
|
||||
|
||||
use CondorcetPHP\Condorcet\Vote;
|
||||
use CondorcetPHP\Condorcet\Throwable\{FileDoesNotExistException, VoteException, VoteInvalidFormatException, VoteMaxNumberReachedException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, InternalModulesAPI, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\DataManager\VotesManager;
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\CondorcetInternalException;
|
||||
use CondorcetPHP\Condorcet\Utils\{CondorcetUtil, VoteEntryParser, VoteUtil};
|
||||
|
||||
// Manage Results for Election class
|
||||
trait VotesProcess
|
||||
{
|
||||
/////////// CONSTRUCTOR ///////////
|
||||
|
||||
// Data and global options
|
||||
protected VotesManager $Votes; // Votes list
|
||||
protected int $voteFastMode = 0; // When parsing vote, avoid unnecessary checks
|
||||
|
||||
|
||||
/////////// VOTES LIST ///////////
|
||||
|
||||
// How many votes are registered ?
|
||||
#[PublicAPI]
|
||||
#[Description('Count the number of actual registered and valid vote for this election. This method ignore votes constraints, only valid vote will be counted.')]
|
||||
#[FunctionReturn('Number of valid and registered vote into this election.')]
|
||||
#[Related('Election::getVotesList', 'Election::countValidVoteWithConstraints')]
|
||||
public function countVotes(
|
||||
#[FunctionParameter('Tag into string separated by commas, or an Array')]
|
||||
array|null|string $tags = null,
|
||||
#[FunctionParameter('Count Votes with this tag ou without this tag-')]
|
||||
bool $with = true
|
||||
): int {
|
||||
return $this->Votes->countVotes(VoteUtil::tagsConvert($tags), $with);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Count the number of actual invalid (if constraints functionality is enabled) but registered vote for this election.')]
|
||||
#[FunctionReturn('Number of valid and registered vote into this election.')]
|
||||
#[Related('Election::countValidVoteWithConstraints', 'Election::countVotes', 'Election::sumValidVotesWeightWithConstraints')]
|
||||
public function countInvalidVoteWithConstraints(): int
|
||||
{
|
||||
return $this->Votes->countInvalidVoteWithConstraints();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Count the number of actual registered and valid vote for this election. This method don't ignore votes constraints, only valid vote will be counted.")]
|
||||
#[FunctionReturn('Number of valid and registered vote into this election.')]
|
||||
#[Related('Election::countInvalidVoteWithConstraints', 'Election::countVotes', 'Election::sumValidVotesWeightWithConstraints')]
|
||||
public function countValidVoteWithConstraints(): int
|
||||
{
|
||||
return $this->countVotes() - $this->countInvalidVoteWithConstraints();
|
||||
}
|
||||
|
||||
// Sum votes weight
|
||||
#[PublicAPI]
|
||||
#[Description('Sum total votes weight in this election. If vote weight functionality is disable (default setting), it will return the number of registered votes. This method ignore votes constraints.')]
|
||||
#[FunctionReturn('(Int) Total vote weight')]
|
||||
#[Related('Election::sumValidVotesWeightWithConstraints')]
|
||||
public function sumVotesWeight(): int
|
||||
{
|
||||
return $this->Votes->sumVotesWeight(false);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Sum total votes weight in this election. If vote weight functionality is disable (default setting), it will return the number of registered votes. This method don't ignore votes constraints, only valid vote will be counted.")]
|
||||
#[FunctionReturn('(Int) Total vote weight')]
|
||||
#[Related('Election::countValidVoteWithConstraints', 'Election::countInvalidVoteWithConstraints')]
|
||||
public function sumValidVotesWeightWithConstraints(): int
|
||||
{
|
||||
return $this->Votes->sumVotesWeight(true);
|
||||
}
|
||||
|
||||
// Get the votes registered list
|
||||
#[PublicAPI]
|
||||
#[Description('Get registered vote list.')]
|
||||
#[FunctionReturn('Populated by each Vote object.')]
|
||||
#[Related('Election::countVotes', 'Election::getVotesListAsString')]
|
||||
public function getVotesList(
|
||||
#[FunctionParameter('Tags list as a string separated by commas or array')]
|
||||
array|null|string $tags = null,
|
||||
#[FunctionParameter('Get votes with these tags or without')]
|
||||
bool $with = true
|
||||
): array {
|
||||
return $this->Votes->getVotesList(VoteUtil::tagsConvert($tags), $with);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get registered vote list.')]
|
||||
#[FunctionReturn("Return a string like :<br>\nA > B > C * 3<br>\nA = B > C * 6")]
|
||||
#[Related('Election::parseVotes')]
|
||||
public function getVotesListAsString(
|
||||
#[FunctionParameter('Depending of the implicit ranking rule of the election, will complete or not the ranking. If $withContext is false, ranking are never adapted to the context.')]
|
||||
bool $withContext = true
|
||||
): string {
|
||||
return $this->Votes->getVotesListAsString($withContext);
|
||||
}
|
||||
|
||||
public function getVotesManager(): VotesManager
|
||||
{
|
||||
return $this->Votes;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Same as Election::getVotesList. But Return a PHP generator object.\nUsefull if your work on very large election with an external DataHandler, because it's will not using large memory amount.")]
|
||||
#[FunctionReturn('Populated by each Vote object.')]
|
||||
#[Related('Election::getVotesList')]
|
||||
public function getVotesListGenerator(
|
||||
#[FunctionParameter('Tags list as a string separated by commas or array')]
|
||||
array|null|string $tags = null,
|
||||
#[FunctionParameter('Get votes with these tags or without')]
|
||||
bool $with = true
|
||||
): \Generator {
|
||||
return $this->Votes->getVotesListGenerator(VoteUtil::tagsConvert($tags), $with);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Same as Election::getVotesList. But Return a PHP generator object.\nUsefull if your work on very large election with an external DataHandler, because it's will not using large memory amount.")]
|
||||
#[FunctionReturn('Populated by each Vote object.')]
|
||||
#[Related('Election::getVotesListGenerator', 'Election::getVotesList')]
|
||||
public function getVotesValidUnderConstraintGenerator(
|
||||
#[FunctionParameter('Tags list as a string separated by commas or array')]
|
||||
array|null|string $tags = null,
|
||||
#[FunctionParameter('Get votes with these tags or without')]
|
||||
bool $with = true
|
||||
): \Generator {
|
||||
return $this->Votes->getVotesValidUnderConstraintGenerator($tags, $with);
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getVoteKey(Vote $vote): ?int
|
||||
{
|
||||
return $this->Votes->getVoteKey($vote);
|
||||
}
|
||||
|
||||
|
||||
/////////// ADD & REMOVE VOTE ///////////
|
||||
|
||||
// Add a single vote. Array key is the rank, each candidate in a rank are separate by ',' It is not necessary to register the last rank.
|
||||
#[PublicAPI]
|
||||
#[Description('Add a vote to an election.')]
|
||||
#[FunctionReturn('The vote object.')]
|
||||
#[Throws(VoteMaxNumberReachedException::class)]
|
||||
#[Example('Manual - Vote Management', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Election::parseVotes', 'Election::addVotesFromJson', 'Election::removeVote', 'Election::getVotesList')]
|
||||
public function addVote(
|
||||
#[FunctionParameter('String or array representation. Or CondorcetPHP\Condorcet\Vote object. If you not provide yourself Vote object, a new one will be generate for you')]
|
||||
array|string|Vote $vote,
|
||||
#[FunctionParameter('String separated by commas or an array. Will add tags to the vote object for you. But you can too add it yourself to Vote object')]
|
||||
array|string|null $tags = null
|
||||
): Vote {
|
||||
$this->prepareVoteInput($vote, $tags);
|
||||
|
||||
// Check Max Vote Count
|
||||
if (self::$maxVoteNumber !== null && $this->countVotes() >= self::$maxVoteNumber) {
|
||||
throw new VoteMaxNumberReachedException(self::$maxVoteNumber);
|
||||
}
|
||||
|
||||
// Register vote
|
||||
return $this->registerVote($vote, $tags); // Return the vote object
|
||||
}
|
||||
|
||||
public function prepareUpdateVote(Vote $existVote): void
|
||||
{
|
||||
$this->Votes->UpdateAndResetComputing(key: $this->getVoteKey($existVote), type: 2);
|
||||
}
|
||||
|
||||
public function finishUpdateVote(Vote $existVote): void
|
||||
{
|
||||
$this->Votes->UpdateAndResetComputing(key: $this->getVoteKey($existVote), type: 1);
|
||||
|
||||
if ($this->Votes->isUsingHandler()) {
|
||||
$this->Votes[$this->getVoteKey($existVote)] = $existVote;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkVoteCandidate(Vote $vote): bool
|
||||
{
|
||||
if ($this->voteFastMode === 0) {
|
||||
$linkCount = $vote->countLinks();
|
||||
$linkCheck = ($linkCount === 0 || ($linkCount === 1 && $vote->haveLink($this)));
|
||||
|
||||
foreach ($vote->getAllCandidates() as $candidate) {
|
||||
if (!$linkCheck && $candidate->getProvisionalState() && !$this->isRegisteredCandidate(candidate: $candidate, strictMode: true) && $this->isRegisteredCandidate(candidate: $candidate, strictMode: false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->voteFastMode < 2) {
|
||||
$ranking = $vote->getRanking();
|
||||
|
||||
$change = $this->convertRankingCandidates($ranking);
|
||||
|
||||
if ($change) {
|
||||
$vote->setRanking(
|
||||
$ranking,
|
||||
(abs($vote->getTimestamp() - microtime(true)) > 0.5) ? ($vote->getTimestamp() + 0.001) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function convertRankingCandidates(array &$ranking): bool
|
||||
{
|
||||
$change = false;
|
||||
|
||||
foreach ($ranking as &$choice) {
|
||||
foreach ($choice as &$candidate) {
|
||||
if (!$this->isRegisteredCandidate($candidate, true)) {
|
||||
if ($candidate->getProvisionalState() && $this->isRegisteredCandidate(candidate: $candidate, strictMode: false)) {
|
||||
$candidate = $this->Candidates[$this->getCandidateKey((string) $candidate)];
|
||||
$change = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $change;
|
||||
}
|
||||
|
||||
// Write a new vote
|
||||
protected function registerVote(Vote $vote, array|string|null $tags): Vote
|
||||
{
|
||||
// Vote identifiant
|
||||
$tags === null || $vote->addTags($tags);
|
||||
|
||||
// Register
|
||||
try {
|
||||
$vote->registerLink($this);
|
||||
$this->Votes[] = $vote;
|
||||
} catch (CondorcetInternalException $e) {
|
||||
// Security : Check if vote object is not already registered
|
||||
throw new VoteException('seats are already registered');
|
||||
}
|
||||
|
||||
return $vote;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Remove Votes from an election.')]
|
||||
#[FunctionReturn("List of removed CondorcetPHP\Condorcet\Vote object.")]
|
||||
#[Example('Manual - Vote management', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-2.-Manage-Vote')]
|
||||
#[Related('Election::addVote', 'Election::getVotesList', 'Election::removeVotesByTags')]
|
||||
public function removeVote(
|
||||
#[FunctionParameter('Vote object')]
|
||||
Vote $vote
|
||||
): bool {
|
||||
$key = $this->getVoteKey($vote);
|
||||
if ($key !== null) {
|
||||
$deletedVote = $this->Votes[$key];
|
||||
|
||||
unset($this->Votes[$key]);
|
||||
|
||||
$deletedVote->destroyLink($this);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new VoteException('cannot remove vote, is not registered in this election');
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Remove Vote from an election using tags.\n\n```php\n\$election->removeVotesByTags('Charlie') ; // Remove vote(s) with tag Charlie\n\$election->removeVotesByTags('Charlie', false) ; // Remove votes without tag Charlie\n\$election->removeVotesByTags('Charlie, Julien', false) ; // Remove votes without tag Charlie AND without tag Julien.\n\$election->removeVotesByTags(array('Julien','Charlie')) ; // Remove votes with tag Charlie OR with tag Julien.\n```")]
|
||||
#[FunctionReturn("List of removed CondorcetPHP\Condorcet\Vote object.")]
|
||||
#[Example('Manual - Vote management', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-2.-Manage-Vote')]
|
||||
#[Related('Election::addVote', 'Election::getVotesList', 'Election::removeVotes')]
|
||||
public function removeVotesByTags(
|
||||
#[FunctionParameter('Tags as string separated by commas or array')]
|
||||
array|string $tags,
|
||||
#[FunctionParameter('Votes with these tags or without')]
|
||||
bool $with = true
|
||||
): array {
|
||||
$rem = [];
|
||||
|
||||
// Prepare Tags
|
||||
$tags = VoteUtil::tagsConvert($tags);
|
||||
|
||||
// Deleting
|
||||
foreach ($this->getVotesList($tags, $with) as $key => $value) {
|
||||
$deletedVote = $this->Votes[$key];
|
||||
$rem[] = $deletedVote;
|
||||
|
||||
unset($this->Votes[$key]);
|
||||
|
||||
$deletedVote->destroyLink($this);
|
||||
}
|
||||
|
||||
return $rem;
|
||||
}
|
||||
|
||||
|
||||
/////////// PARSE VOTE ///////////
|
||||
|
||||
// Return the well formatted vote to use.
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
protected function prepareVoteInput(array|string|Vote &$vote, array|string|null $tags = null): void
|
||||
{
|
||||
if (!($vote instanceof Vote)) {
|
||||
$vote = new Vote(ranking: $vote, tags: $tags, ownTimestamp: null, electionContext: $this);
|
||||
}
|
||||
|
||||
// Check array format && Make checkVoteCandidate
|
||||
if (!$this->checkVoteCandidate($vote)) {
|
||||
throw new VoteInvalidFormatException;
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Import votes from a Json source.')]
|
||||
#[FunctionReturn('Count of new registered vote.')]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Election::addVote', 'Election::parseVotes', 'Election::addCandidatesFromJson')]
|
||||
public function addVotesFromJson(
|
||||
#[FunctionParameter('Json string input')]
|
||||
string $input
|
||||
): int {
|
||||
$input = CondorcetUtil::prepareJson($input);
|
||||
|
||||
$adding = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($input as $record) {
|
||||
if (empty($record['vote'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vote = $record['vote'];
|
||||
$tags = !isset($record['tag']) ? null : $record['tag'];
|
||||
$multiple = !isset($record['multi']) ? 1 : (int) $record['multi'];
|
||||
$weight = !isset($record['weight']) ? 1 : (int) $record['weight'];
|
||||
|
||||
$this->synthesisVoteFromParse($count, $multiple, $adding, $vote, $tags, $weight);
|
||||
}
|
||||
|
||||
$this->doAddVotesFromParse($adding);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Import votes from a text source. If any invalid vote is found inside, nothing are registered.')]
|
||||
#[FunctionReturn('Count of the new registered vote.')]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Election::addVote', 'Election::parseCandidates', 'Election::parseVotesWithoutFail', 'Election::addVotesFromJson')]
|
||||
public function parseVotes(
|
||||
#[FunctionParameter('String or valid path to a text file')]
|
||||
string $input,
|
||||
#[FunctionParameter('If true, the input is evalatued as path to text file')]
|
||||
bool $isFile = false
|
||||
): int {
|
||||
$input = CondorcetUtil::prepareParse($input, $isFile);
|
||||
|
||||
$adding = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($input as $line) {
|
||||
$voteParser = new VoteEntryParser($line);
|
||||
|
||||
$this->synthesisVoteFromParse(
|
||||
count: $count,
|
||||
multiple: $voteParser->multiple,
|
||||
adding: $adding,
|
||||
vote: $voteParser->ranking,
|
||||
tags: $voteParser->tags,
|
||||
weight: $voteParser->weight,
|
||||
);
|
||||
}
|
||||
|
||||
$this->doAddVotesFromParse($adding);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Similar to parseVote method. But will ignore invalid line. This method is also far less greedy in memory and must be prefered for very large file input. And to combine with the use of an external data handler.')]
|
||||
#[FunctionReturn("Number of invalid records into input (except empty lines). It's not an invalid votes count. Check Election::countVotes if you want to be sure.")]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Election::addVote', 'Election::parseCandidates', 'Election::parseVotes', 'Election::addVotesFromJson')]
|
||||
public function parseVotesWithoutFail(
|
||||
#[FunctionParameter('String, valid path to a text file or an object SplFileInfo or extending it like SplFileObject')]
|
||||
\SplFileInfo|string $input,
|
||||
#[FunctionParameter('If true, the string input is evalatued as path to text file')]
|
||||
bool $isFile = false,
|
||||
#[FunctionParameter('Callback function to execute after each registered vote')]
|
||||
?\Closure $callBack = null
|
||||
): int {
|
||||
$inserted_votes_count = 0;
|
||||
$fail_count = 0;
|
||||
$doCallBack = $callBack !== null;
|
||||
|
||||
if (!$isFile && !($input instanceof \SplFileInfo)) {
|
||||
$file = new \SplTempFileObject(256 * 1024 * 1024);
|
||||
$file->fwrite($input);
|
||||
} elseif ($input instanceof \SplFileObject) {
|
||||
$file = $input;
|
||||
} else {
|
||||
$file = ($input instanceof \SplFileInfo) ? $input : new \SplFileInfo($input);
|
||||
|
||||
if ($file->isFile() && $file->isReadable()) {
|
||||
$file = ($file instanceof \SplFileObject) ? $file : $file->openFile('r');
|
||||
} else {
|
||||
throw new FileDoesNotExistException('Specified input file does not exist. path: '.$input);
|
||||
}
|
||||
}
|
||||
|
||||
unset($input); // Memory Optimization
|
||||
$file->setFlags($file->getFlags() | \SplFileObject::SKIP_EMPTY);
|
||||
$file->rewind();
|
||||
|
||||
$char = '';
|
||||
$record = '';
|
||||
|
||||
while ($char !== false) {
|
||||
$char = $file->fgetc();
|
||||
|
||||
if ($char === ';' || $char === "\n" || $char === '#' || $char === false) {
|
||||
if ($char === '#') {
|
||||
$record .= $char;
|
||||
|
||||
while (($char = $file->fgetc()) !== false) {
|
||||
if ($char === "\n") {
|
||||
break;
|
||||
} else {
|
||||
$record .= $char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedVote = new VoteEntryParser($record);
|
||||
|
||||
if ($parsedVote->ranking === null) {
|
||||
$record = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$adding = [];
|
||||
|
||||
$this->synthesisVoteFromParse(
|
||||
count: $count,
|
||||
adding: $adding,
|
||||
vote: $parsedVote->ranking,
|
||||
tags: $parsedVote->tags,
|
||||
weight: $parsedVote->weight,
|
||||
multiple: $parsedVote->multiple
|
||||
);
|
||||
|
||||
$this->doAddVotesFromParse($adding);
|
||||
|
||||
$inserted_votes_count += $count;
|
||||
|
||||
if ($doCallBack) {
|
||||
$doCallBack = $callBack($inserted_votes_count);
|
||||
}
|
||||
} catch (VoteInvalidFormatException) {
|
||||
++$fail_count;
|
||||
} finally {
|
||||
$record = '';
|
||||
}
|
||||
} else {
|
||||
$record .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
return $fail_count;
|
||||
}
|
||||
|
||||
protected function synthesisVoteFromParse(int &$count, int $multiple, array &$adding, array|string|Vote $vote, null|array|string $tags, int $weight): void
|
||||
{
|
||||
$adding_predicted_count = $count + $multiple;
|
||||
|
||||
if (self::$maxVoteNumber && self::$maxVoteNumber < ($this->countVotes() + $adding_predicted_count)) {
|
||||
throw new VoteMaxNumberReachedException(self::$maxParseIteration);
|
||||
}
|
||||
|
||||
if (self::$maxParseIteration !== null && $adding_predicted_count >= self::$maxParseIteration) {
|
||||
throw new VoteMaxNumberReachedException(self::$maxParseIteration);
|
||||
}
|
||||
|
||||
$newVote = new Vote(ranking: $vote, tags: $tags, electionContext: $this);
|
||||
$newVote->setWeight($weight);
|
||||
|
||||
$adding[] = ['multiple' => $multiple, 'vote' => $newVote];
|
||||
|
||||
$count += $multiple;
|
||||
}
|
||||
|
||||
protected function doAddVotesFromParse(array $adding): void
|
||||
{
|
||||
$this->voteFastMode = 1;
|
||||
|
||||
foreach ($adding as $oneLine) {
|
||||
for ($i = 1; $i <= $oneLine['multiple']; $i++) {
|
||||
if ($i === 1) {
|
||||
$finalVoteModel = $this->addVote($oneLine['vote']);
|
||||
$this->voteFastMode = 2;
|
||||
} else {
|
||||
$this->addVote(clone $finalVoteModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->voteFastMode = 0;
|
||||
}
|
||||
}
|
93
include/condorcet/src/Linkable.php
Normal file
93
include/condorcet/src/Linkable.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related};
|
||||
use CondorcetPHP\Condorcet\Throwable\Internal\CondorcetInternalException;
|
||||
|
||||
trait Linkable
|
||||
{
|
||||
private ?\WeakMap $link;
|
||||
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->destroyAllLink();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Check if this election is linked with this Candidate/Vote object.')]
|
||||
#[FunctionReturn('True or False.')]
|
||||
#[Related('Vote::countLinks', 'Candidate::countLinks', 'Vote::getLinks', 'Candidate::getLinks', 'Vote::haveLink', 'Candidate::haveLink')]
|
||||
public function haveLink(
|
||||
#[FunctionParameter('Condorcet election to check')]
|
||||
Election $election
|
||||
): bool {
|
||||
$this->initWeakMap();
|
||||
|
||||
return $this->link->offsetExists($election);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Count number of linked election to this object.')]
|
||||
#[FunctionReturn('Number of linked elections.')]
|
||||
#[Related('Vote::countLinks', 'Candidate::countLinks', 'Vote::getLinks', 'Candidate::getLinks', 'Vote::haveLink', 'Candidate::haveLink')]
|
||||
public function countLinks(): int
|
||||
{
|
||||
$this->initWeakMap();
|
||||
|
||||
return \count($this->link);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get elections object linked to this Vote or Candidate object.')]
|
||||
#[FunctionReturn('Populated by each elections Condorcet object.')]
|
||||
#[Related('Vote::countLinks', 'Candidate::countLinks', 'Vote::getLinks', 'Candidate::getLinks', 'Vote::haveLink', 'Candidate::haveLink')]
|
||||
public function getLinks(): \WeakMap
|
||||
{
|
||||
$this->initWeakMap();
|
||||
|
||||
return $this->link;
|
||||
}
|
||||
|
||||
// Internal
|
||||
# Dot not Overloading ! Do not Use !
|
||||
|
||||
protected function initWeakMap(): void
|
||||
{
|
||||
$this->link ??= new \WeakMap;
|
||||
}
|
||||
|
||||
public function registerLink(Election $election): void
|
||||
{
|
||||
if (!$this->haveLink($election)) { // haveLink will initWeakmap if necessary
|
||||
$this->link->offsetSet($election, true);
|
||||
} else {
|
||||
throw new CondorcetInternalException('Link is already registered.');
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyLink(Election $election): bool
|
||||
{
|
||||
if ($this->haveLink($election)) { // haveLink will initWeakmap if necessary
|
||||
$this->link->offsetUnset($election);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function destroyAllLink(): void
|
||||
{
|
||||
$this->link = null;
|
||||
$this->initWeakMap();
|
||||
}
|
||||
}
|
354
include/condorcet/src/Result.php
Normal file
354
include/condorcet/src/Result.php
Normal file
@ -0,0 +1,354 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Algo\StatsVerbosity;
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, InternalModulesAPI, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\Utils\{CondorcetUtil, VoteUtil};
|
||||
use CondorcetPHP\Condorcet\Throwable\ResultException;
|
||||
|
||||
class Result implements \ArrayAccess, \Countable, \Iterator
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
// Implement Iterator
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
reset($this->ResultIterator);
|
||||
}
|
||||
|
||||
public function current(): array|Candidate
|
||||
{
|
||||
return current($this->ResultIterator);
|
||||
}
|
||||
|
||||
public function key(): int
|
||||
{
|
||||
return key($this->ResultIterator);
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
next($this->ResultIterator);
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return key($this->ResultIterator) !== null;
|
||||
}
|
||||
|
||||
// Implement ArrayAccess
|
||||
|
||||
#[Throws(ResultException::class)]
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
throw new ResultException;
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->ranking[$offset]);
|
||||
}
|
||||
|
||||
#[Throws(ResultException::class)]
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new ResultException;
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): array|Candidate|null
|
||||
{
|
||||
return $this->ranking[$offset] ?? null;
|
||||
}
|
||||
|
||||
// Implement Countable
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->ranking);
|
||||
}
|
||||
|
||||
|
||||
/////////// CONSTRUCTOR ///////////
|
||||
|
||||
protected readonly array $Result;
|
||||
protected array $ResultIterator;
|
||||
protected $Stats;
|
||||
protected array $warning = [];
|
||||
|
||||
#[PublicAPI]
|
||||
public readonly array $ranking;
|
||||
#[PublicAPI]
|
||||
public readonly array $rankingAsString;
|
||||
#[PublicAPI]
|
||||
public readonly ?int $seats;
|
||||
#[PublicAPI]
|
||||
public readonly array $methodOptions;
|
||||
#[PublicAPI]
|
||||
public readonly ?Candidate $CondorcetWinner;
|
||||
#[PublicAPI]
|
||||
public readonly ?Candidate $CondorcetLoser;
|
||||
#[PublicAPI]
|
||||
public readonly float $buildTimestamp;
|
||||
#[PublicAPI]
|
||||
public readonly string $fromMethod;
|
||||
#[PublicAPI]
|
||||
public readonly string $byClass;
|
||||
#[PublicAPI]
|
||||
public readonly StatsVerbosity $statsVerbosity;
|
||||
#[PublicAPI]
|
||||
public readonly string $electionCondorcetVersion;
|
||||
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function __construct(string $fromMethod, string $byClass, Election $election, array $result, $stats, ?int $seats = null, array $methodOptions = [])
|
||||
{
|
||||
ksort($result, \SORT_NUMERIC);
|
||||
|
||||
$this->Result = $result;
|
||||
$this->ResultIterator = $this->ranking = $this->makeUserResult($election);
|
||||
$this->rankingAsString = $this->getResultAsArray(true);
|
||||
$this->Stats = $stats;
|
||||
$this->seats = $seats;
|
||||
$this->fromMethod = $fromMethod;
|
||||
$this->byClass = $byClass;
|
||||
$this->statsVerbosity = $election->getStatsVerbosity();
|
||||
$this->electionCondorcetVersion = $election->getObjectVersion();
|
||||
$this->CondorcetWinner = $election->getWinner();
|
||||
$this->CondorcetLoser = $election->getLoser();
|
||||
$this->buildTimestamp = microtime(true);
|
||||
$this->methodOptions = $methodOptions;
|
||||
}
|
||||
|
||||
|
||||
/////////// Get Result ///////////
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get result as an array')]
|
||||
#[FunctionReturn('An ordered multidimensionnal array by rank.')]
|
||||
#[Related('Election::getResult', 'Result::getResultAsString')]
|
||||
public function getResultAsArray(
|
||||
#[FunctionParameter('Convert Candidate object to string')]
|
||||
bool $convertToString = false
|
||||
): array {
|
||||
$r = $this->ranking;
|
||||
|
||||
foreach ($r as &$rank) {
|
||||
if (\count($rank) === 1) {
|
||||
$rank = $convertToString ? (string) $rank[0] : $rank[0];
|
||||
} elseif ($convertToString) {
|
||||
foreach ($rank as &$subRank) {
|
||||
$subRank = (string) $subRank;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get result as string')]
|
||||
#[FunctionReturn('Result ranking as string.')]
|
||||
#[Related('Election::getResult', 'Result::getResultAsArray')]
|
||||
public function getResultAsString(): string
|
||||
{
|
||||
return VoteUtil::getRankingAsString($this->getResultAsArray(true));
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get result as an array')]
|
||||
#[FunctionReturn("Unlike other methods to recover the result. This is frozen as soon as the original creation of the Result object is created.\nCandidate objects are therefore protected from any change of candidateName, since the candidate objects are converted into a string when the results are promulgated.\n\nThis control method can therefore be useful if you undertake suspicious operations on candidate objects after the results have been promulgated.")]
|
||||
#[Related('Result::getResultAsArray', 'Result::getResultAsString')]
|
||||
public function getOriginalResultArrayWithString(): array
|
||||
{
|
||||
return $this->rankingAsString;
|
||||
}
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function getResultAsInternalKey(): array
|
||||
{
|
||||
return $this->Result;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get advanced computing data from used algorithm. Like Strongest paths for Schulze method.')]
|
||||
#[FunctionReturn('Varying according to the algorithm used.')]
|
||||
#[Example('Advanced Result Management', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-C.-Result-%23-3.-Advanced-Results-Management')]
|
||||
#[Related('Election::getResult')]
|
||||
public function getStats(): mixed
|
||||
{
|
||||
return $this->Stats;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Equivalent to [Condorcet/Election::getWinner($method)](../Election Class/public Election--getWinner.md).')]
|
||||
#[FunctionReturn("Candidate object given. Null if there are no available winner.\nYou can get an array with multiples winners.")]
|
||||
#[Related('Result::getLoser', 'Election::getWinner')]
|
||||
public function getWinner(): array|Candidate|null
|
||||
{
|
||||
return CondorcetUtil::format($this[1], false);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Equivalent to [Condorcet/Election::getWinner($method)](../Election Class/public Election--getWinner.md).')]
|
||||
#[FunctionReturn("Candidate object given. Null if there are no available loser.\nYou can get an array with multiples losers.")]
|
||||
#[Related('Result::getWinner', 'Election::getLoser')]
|
||||
public function getLoser(): array|Candidate|null
|
||||
{
|
||||
return CondorcetUtil::format($this[\count($this)], false);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the Condorcet winner, if exist, at the result time.')]
|
||||
#[FunctionReturn("CondorcetPHP\Condorcet\Candidate object if there is a Condorcet winner or NULL instead.")]
|
||||
#[Related('Result::getCondorcetLoser', 'Election::getWinner')]
|
||||
public function getCondorcetWinner(): ?Candidate
|
||||
{
|
||||
return $this->CondorcetWinner;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the Condorcet loser, if exist, at the result time.')]
|
||||
#[FunctionReturn('Condorcet/Candidate object if there is a Condorcet loser or NULL instead.')]
|
||||
#[Related('Result::getCondorcetWinner', 'Election::getLoser')]
|
||||
public function getCondorcetLoser(): ?Candidate
|
||||
{
|
||||
return $this->CondorcetLoser;
|
||||
}
|
||||
|
||||
protected function makeUserResult(Election $election): array
|
||||
{
|
||||
$userResult = [];
|
||||
|
||||
foreach ($this->Result as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
foreach ($value as $candidate_key) {
|
||||
$userResult[$key][] = $election->getCandidateObjectFromKey($candidate_key);
|
||||
}
|
||||
|
||||
sort($userResult[$key], \SORT_STRING);
|
||||
} elseif ($value === null) {
|
||||
$userResult[$key] = null;
|
||||
} else {
|
||||
$userResult[$key][] = $election->getCandidateObjectFromKey($value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($userResult as $key => $value) {
|
||||
if ($value === null) {
|
||||
$userResult[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $userResult;
|
||||
}
|
||||
|
||||
|
||||
/////////// Get & Set MetaData ///////////
|
||||
|
||||
#[InternalModulesAPI]
|
||||
public function addWarning(int $type, ?string $msg = null): bool
|
||||
{
|
||||
$this->warning[] = ['type' => $type, 'msg' => $msg];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('From native methods: only Kemeny-Young use it to inform about a conflict during the computation process.')]
|
||||
#[FunctionReturn('Warnings provided by the by the method that generated the warning. Empty array if there is not.')]
|
||||
public function getWarning(
|
||||
#[FunctionParameter('Filter on a specific warning type code')]
|
||||
?int $type = null
|
||||
): array {
|
||||
if ($type === null) {
|
||||
return $this->warning;
|
||||
} else {
|
||||
$r = [];
|
||||
|
||||
foreach ($this->warning as $oneWarning) {
|
||||
if ($oneWarning['type'] === $type) {
|
||||
$r[] = $oneWarning;
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the The algorithmic method used for this result.')]
|
||||
#[FunctionReturn("Method class path like CondorcetPHP\Condorcet\Algo\Methods\Copeland")]
|
||||
#[Related('Result::getMethod')]
|
||||
public function getClassGenerator(): string
|
||||
{
|
||||
return $this->byClass;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the The algorithmic method used for this result.')]
|
||||
#[FunctionReturn('Method name.')]
|
||||
#[Related('Result::getClassGenerator')]
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->fromMethod;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the method options.')]
|
||||
#[FunctionReturn('Array of options. Can be empty for most of the methods.')]
|
||||
#[Related('Result::getClassGenerator')]
|
||||
public function getMethodOptions(): array
|
||||
{
|
||||
$r = $this->methodOptions;
|
||||
|
||||
if ($this->isProportional()) {
|
||||
$r['Seats'] = $this->getNumberOfSeats();
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the timestamp of this result.')]
|
||||
#[FunctionReturn('Microsecond timestamp.')]
|
||||
public function getBuildTimeStamp(): float
|
||||
{
|
||||
return (float) $this->buildTimestamp;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the Condorcet PHP version that build this Result.')]
|
||||
#[FunctionReturn('Condorcet PHP version string format.')]
|
||||
public function getCondorcetElectionGeneratorVersion(): string
|
||||
{
|
||||
return $this->electionCondorcetVersion;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get number of Seats for STV methods result.')]
|
||||
#[FunctionReturn('Number of seats if this result is a STV method. Else NULL.')]
|
||||
#[Related('Election::setNumberOfSeats', 'Election::getNumberOfSeats')]
|
||||
public function getNumberOfSeats(): ?int
|
||||
{
|
||||
return $this->seats;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Does the result come from a proportional method')]
|
||||
#[Related('Result::getNumberOfSeats')]
|
||||
public function isProportional(): bool
|
||||
{
|
||||
return $this->seats !== null;
|
||||
}
|
||||
}
|
17
include/condorcet/src/Throwable/AlgorithmException.php
Normal file
17
include/condorcet/src/Throwable/AlgorithmException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class AlgorithmException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The voting algorithm is not available';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class AlgorithmWithoutRankingFeatureException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = "This algortihm can't provide a full ranking (but only Winner and Loser)";
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class CandidateDoesNotExistException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'This candidate does not exist';
|
||||
}
|
17
include/condorcet/src/Throwable/CandidateExistsException.php
Normal file
17
include/condorcet/src/Throwable/CandidateExistsException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class CandidateExistsException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'This candidate already exists';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class CandidateInvalidNameException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'This name is not valid';
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class CandidatesMaxNumberReachedException extends MethodLimitReachedException
|
||||
{
|
||||
protected $message = 'Maximum number of candidates reached';
|
||||
|
||||
public function __construct(string $method, int $maxCandidates)
|
||||
{
|
||||
parent::__construct($method, "{$this->message}: The method '{$method}' is configured to accept only {$maxCandidates} candidates");
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?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\Throwable;
|
||||
|
||||
use CondorcetPHP\Condorcet\CondorcetVersion;
|
||||
|
||||
// Custom Exception
|
||||
abstract class CondorcetPublicApiException extends \Exception implements \Stringable
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
public function __construct(int|string|null $message = null)
|
||||
{
|
||||
// If there is a custom message, append it.
|
||||
if ($message !== null) {
|
||||
$this->message .= ': ' . $message;
|
||||
}
|
||||
|
||||
parent::__construct($this->message);
|
||||
}
|
||||
}
|
17
include/condorcet/src/Throwable/DataHandlerException.php
Normal file
17
include/condorcet/src/Throwable/DataHandlerException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class DataHandlerException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Problem with data handler';
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?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\Throwable;
|
||||
|
||||
use CondorcetPHP\Condorcet\Condorcet;
|
||||
|
||||
class ElectionObjectVersionMismatchException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Version mismatch';
|
||||
|
||||
public function __construct(string $message = '')
|
||||
{
|
||||
parent::__construct(
|
||||
"The election object has version '{$message}' " .
|
||||
'which is different from the current class ' .
|
||||
"version '" . Condorcet::getVersion(true) . "'"
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class FileDoesNotExistException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Specified input file does not exist';
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?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\Throwable\Internal;
|
||||
|
||||
use CondorcetPHP\Condorcet\CondorcetVersion;
|
||||
|
||||
// Custom Exeption
|
||||
class CondorcetInternalError extends \Error
|
||||
{
|
||||
use CondorcetVersion;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable\Internal;
|
||||
|
||||
// Custom Exeption
|
||||
class CondorcetInternalException extends \Exception
|
||||
{
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable\Internal;
|
||||
|
||||
// Custom Exeption
|
||||
class IntegerOverflowException extends CondorcetInternalException
|
||||
{
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?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\Throwable\Internal;
|
||||
|
||||
class NoGitShellException extends CondorcetInternalException
|
||||
{
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class MethodLimitReachedException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Method limit reached';
|
||||
public readonly string $method;
|
||||
|
||||
public function __construct(string $method, ?string $message = null)
|
||||
{
|
||||
$this->method = $method;
|
||||
|
||||
if ($message !== null) {
|
||||
$this->message = $message;
|
||||
}
|
||||
}
|
||||
}
|
17
include/condorcet/src/Throwable/NoCandidatesException.php
Normal file
17
include/condorcet/src/Throwable/NoCandidatesException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class NoCandidatesException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'You need to specify one or more candidates before voting';
|
||||
}
|
17
include/condorcet/src/Throwable/NoSeatsException.php
Normal file
17
include/condorcet/src/Throwable/NoSeatsException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class NoSeatsException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'No seats defined';
|
||||
}
|
17
include/condorcet/src/Throwable/ResultException.php
Normal file
17
include/condorcet/src/Throwable/ResultException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class ResultException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Result cannot be changed';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class ResultRequestedWithoutVotesException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The result cannot be requested without votes';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class StvQuotaNotImplementedException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'This STV quota is not implemented';
|
||||
}
|
17
include/condorcet/src/Throwable/TimerException.php
Normal file
17
include/condorcet/src/Throwable/TimerException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class TimerException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Only a chrono linked to this manager can be used';
|
||||
}
|
17
include/condorcet/src/Throwable/VoteConstraintException.php
Normal file
17
include/condorcet/src/Throwable/VoteConstraintException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteConstraintException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The vote constraint could not be set up';
|
||||
}
|
17
include/condorcet/src/Throwable/VoteException.php
Normal file
17
include/condorcet/src/Throwable/VoteException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'Problem handling vote';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteInvalidFormatException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The format of the vote is invalid';
|
||||
}
|
17
include/condorcet/src/Throwable/VoteManagerException.php
Normal file
17
include/condorcet/src/Throwable/VoteManagerException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteManagerException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = "Value must be an instance of CondorcetPHP\Vote";
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteMaxNumberReachedException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The maximal number of votes for the method is reached';
|
||||
}
|
17
include/condorcet/src/Throwable/VoteNotLinkedException.php
Normal file
17
include/condorcet/src/Throwable/VoteNotLinkedException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VoteNotLinkedException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The vote is not linked to an election';
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?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\Throwable;
|
||||
|
||||
class VotingHasStartedException extends CondorcetPublicApiException
|
||||
{
|
||||
protected $message = 'The voting has started';
|
||||
}
|
66
include/condorcet/src/Timer/Chrono.php
Normal file
66
include/condorcet/src/Timer/Chrono.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?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\Timer;
|
||||
|
||||
use CondorcetPHP\Condorcet\CondorcetVersion;
|
||||
|
||||
class Chrono
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
protected Manager $manager;
|
||||
protected float $start;
|
||||
protected ?string $role = null;
|
||||
|
||||
public function __construct(Manager $timer, ?string $role = null)
|
||||
{
|
||||
$this->manager = $timer;
|
||||
$this->setRole($role);
|
||||
$this->resetStart();
|
||||
$this->managerStartDeclare();
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->manager->addTime($this);
|
||||
}
|
||||
|
||||
public function getStart(): float
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function getTimerManager(): Manager
|
||||
{
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
protected function resetStart(): void
|
||||
{
|
||||
$this->start = microtime(true);
|
||||
}
|
||||
|
||||
public function getRole(): ?string
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
public function setRole(?string $role): void
|
||||
{
|
||||
$this->role = $role;
|
||||
}
|
||||
|
||||
protected function managerStartDeclare(): void
|
||||
{
|
||||
$this->manager->startDeclare($this);
|
||||
}
|
||||
}
|
85
include/condorcet/src/Timer/Manager.php
Normal file
85
include/condorcet/src/Timer/Manager.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?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\Timer;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionReturn, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\CondorcetVersion;
|
||||
use CondorcetPHP\Condorcet\Throwable\TimerException;
|
||||
|
||||
class Manager
|
||||
{
|
||||
use CondorcetVersion;
|
||||
|
||||
protected float $globalTimer = 0.0;
|
||||
protected ?float $lastTimer = null;
|
||||
protected ?float $lastChronoTimestamp = null;
|
||||
protected ?float $startDeclare = null;
|
||||
protected array $history = [];
|
||||
|
||||
#[Throws(TimerException::class)]
|
||||
public function addTime(Chrono $chrono): void
|
||||
{
|
||||
if ($chrono->getTimerManager() === $this) {
|
||||
if ($this->lastChronoTimestamp === null && $chrono->getStart() !== $this->startDeclare) {
|
||||
return;
|
||||
}
|
||||
|
||||
$m = microtime(true);
|
||||
|
||||
if ($this->lastChronoTimestamp > $chrono->getStart()) {
|
||||
$c = $this->lastChronoTimestamp;
|
||||
} else {
|
||||
$c = $chrono->getStart();
|
||||
$this->history[] = ['role' => $chrono->getRole(),
|
||||
'process_in' => ($m - $c),
|
||||
'timer_start' => $c,
|
||||
'timer_end' => $m,
|
||||
];
|
||||
}
|
||||
|
||||
$this->globalTimer += ($m - $c);
|
||||
|
||||
$this->lastTimer = ($m - $chrono->getStart());
|
||||
$this->lastChronoTimestamp = $m;
|
||||
} else {
|
||||
throw new TimerException;
|
||||
}
|
||||
}
|
||||
|
||||
public function getGlobalTimer(): float
|
||||
{
|
||||
return $this->globalTimer;
|
||||
}
|
||||
|
||||
public function getLastTimer(): float
|
||||
{
|
||||
return $this->lastTimer;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return benchmarked actions history.')]
|
||||
#[FunctionReturn('An explicit array with history.')]
|
||||
#[Related('Election::getTimerManager')]
|
||||
public function getHistory(): array
|
||||
{
|
||||
return $this->history;
|
||||
}
|
||||
|
||||
public function startDeclare(Chrono $chrono): static
|
||||
{
|
||||
if ($this->startDeclare === null) {
|
||||
$this->startDeclare = $chrono->getStart();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
<?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\Tools\Converters;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Candidate, Election};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related};
|
||||
use CondorcetPHP\Condorcet\Throwable\FileDoesNotExistException;
|
||||
use CondorcetPHP\Condorcet\Utils\CondorcetUtil;
|
||||
|
||||
class CondorcetElectionFormat implements ConverterInterface
|
||||
{
|
||||
////// # Static Export Method //////
|
||||
|
||||
public const SPECIAL_KEYWORD_EMPTY_RANKING = '/EMPTY_RANKING/';
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Create a CondorcetElectionFormat file from an Election object.\n")]
|
||||
#[FunctionReturn("If the file is not provided, it's return a CondorcetElectionFormat as string, else returning null and working directly on the file object (necessary for very large non-aggregated elections, at the risk of memory saturation).")]
|
||||
public static function exportElectionToCondorcetElectionFormat(
|
||||
#[FunctionParameter('Election with data')]
|
||||
Election $election,
|
||||
#[FunctionParameter('If true, will try to reduce number of lines, with quantifier for identical votes')]
|
||||
bool $aggregateVotes = true,
|
||||
#[FunctionParameter('Add the Number Of Seats parameters to the output')]
|
||||
bool $includeNumberOfSeats = true,
|
||||
#[FunctionParameter('Add the vote tags information if any. Don\'t work if $aggregateVotes is true')]
|
||||
bool $includeTags = true,
|
||||
#[FunctionParameter('Non-election candidates will be ignored. If the implicit ranking parameter of the election object is true, the last rank will also be provided to facilitate the reading.')]
|
||||
bool $inContext = false,
|
||||
#[FunctionParameter('If provided, the function will return null and the result will be writing directly to the file instead. _Note that the file cursor is not rewinding_')]
|
||||
?\SplFileObject $file = null
|
||||
): ?string {
|
||||
$r = '';
|
||||
$r .= '#/Candidates: ' . implode(' ; ', $election->getCandidatesListAsString()) . "\n";
|
||||
$r .= ($includeNumberOfSeats) ? '#/Number of Seats: ' . $election->getNumberOfSeats() . "\n" : null;
|
||||
$r .= '#/Implicit Ranking: ' . ($election->getImplicitRankingRule() ? 'true' : 'false') . "\n";
|
||||
$r .= '#/Weight Allowed: ' . ($election->isVoteWeightAllowed() ? 'true' : 'false') . "\n";
|
||||
// $r .= "\n";
|
||||
|
||||
if ($file) {
|
||||
$file->fwrite($r);
|
||||
$r = '';
|
||||
}
|
||||
|
||||
if ($aggregateVotes) {
|
||||
$r .= "\n" . $election->getVotesListAsString($inContext);
|
||||
if ($file) {
|
||||
$file->fwrite($r);
|
||||
}
|
||||
} else {
|
||||
foreach ($election->getVotesListGenerator() as $vote) {
|
||||
$line = "\n";
|
||||
$line .= ($includeTags && !empty($vote->getTags())) ? $vote->getTagsAsString().' || ' : '';
|
||||
|
||||
$voteString = $vote->getSimpleRanking($inContext ? $election : null);
|
||||
$line .= !empty($voteString) ? $voteString : self::SPECIAL_KEYWORD_EMPTY_RANKING;
|
||||
|
||||
if ($file) {
|
||||
$file->fwrite($line);
|
||||
} else {
|
||||
$r .= $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ($file) ? null : $r;
|
||||
}
|
||||
|
||||
|
||||
////// # Reader Object //////
|
||||
|
||||
# Reader Object
|
||||
|
||||
// Const
|
||||
protected const CANDIDATES_PATTERN = '/^#\/Candidates:(?<candidates>.+)$/mi';
|
||||
protected const SEATS_PATTERN = '/^#\/Number of Seats: *(?<seats>[0-9]+) *$/mi';
|
||||
protected const IMPLICIT_PATTERN = '/^#\/Implicit Ranking: *(?<implicitRanking>(true|false)) *$/mi';
|
||||
protected const WEIGHT_PATTERN = '/^#\/Weight Allowed: *(?<weight>(true|false)) *$/mi';
|
||||
|
||||
|
||||
// Properties
|
||||
protected \SplFileObject $file;
|
||||
|
||||
#[PublicAPI]
|
||||
public readonly array $candidates;
|
||||
#[PublicAPI]
|
||||
public readonly int $numberOfSeats;
|
||||
#[PublicAPI]
|
||||
public readonly bool $implicitRanking;
|
||||
#[PublicAPI]
|
||||
public readonly bool $voteWeight;
|
||||
|
||||
#[PublicAPI]
|
||||
public readonly bool $CandidatesParsedFromVotes;
|
||||
#[PublicAPI]
|
||||
public readonly int $invalidBlocksCount;
|
||||
|
||||
// Read
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Read a Condorcet format file, usually using .cvotes file extension')]
|
||||
public function __construct(
|
||||
#[FunctionParameter('String, valid path to a text file or an object SplFileInfo or extending it like SplFileObject')]
|
||||
\SplFileInfo|string $input
|
||||
) {
|
||||
$input = ($input instanceof \SplFileInfo) ? $input : new \SplFileInfo($input);
|
||||
|
||||
if ($input instanceof \SplFileObject) {
|
||||
$this->file = $input;
|
||||
} elseif ($input->isFile() && $input->isReadable()) {
|
||||
$this->file = $input->openFile('r');
|
||||
} else {
|
||||
throw new FileDoesNotExistException('Specified input file does not exist. path: '.$input);
|
||||
}
|
||||
|
||||
unset($input); // Memory Optimization
|
||||
|
||||
$this->file->setFlags(\SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE);
|
||||
|
||||
$this->readParameters();
|
||||
|
||||
// Parse candidate directly from votes
|
||||
if (empty($this->candidates)) {
|
||||
$this->parseCandidatesFromVotes();
|
||||
$this->CandidatesParsedFromVotes = true;
|
||||
} else {
|
||||
$this->CandidatesParsedFromVotes = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Add the data to an election object')]
|
||||
#[FunctionReturn('The election object')]
|
||||
#[Related("Tools\DavidHillFormat::setDataToAnElection", "Tools\DebianFormat::setDataToAnElection")]
|
||||
public function setDataToAnElection(
|
||||
#[FunctionParameter('Add an existing election, useful if you want to set up some parameters or add extra candidates. If null an election object will be created for you.')]
|
||||
Election $election = new Election,
|
||||
#[FunctionParameter('Callback function to execute after each registered vote.')]
|
||||
?\Closure $callBack = null
|
||||
): Election {
|
||||
// Parameters
|
||||
// Set number of seats if specified in file
|
||||
($this->numberOfSeats ?? false) && $election->setNumberOfSeats($this->numberOfSeats);
|
||||
|
||||
// Set explicit pairwise mode if specified in file
|
||||
$this->implicitRanking ??= $election->getImplicitRankingRule();
|
||||
$election->setImplicitRanking($this->implicitRanking);
|
||||
|
||||
// Set Vote weight (Condorcet disable it by default)
|
||||
$this->voteWeight ??= $election->isVoteWeightAllowed();
|
||||
$election->allowsVoteWeight($this->voteWeight);
|
||||
|
||||
|
||||
// Candidates
|
||||
foreach ($this->candidates as $oneCandidate) {
|
||||
$election->addCandidate($oneCandidate);
|
||||
}
|
||||
|
||||
// Votes
|
||||
$this->file->rewind();
|
||||
$this->invalidBlocksCount = $election->parseVotesWithoutFail(input: $this->file, callBack: $callBack);
|
||||
|
||||
return $election;
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
protected function addCandidates(array $candidates): void
|
||||
{
|
||||
sort($candidates, \SORT_NATURAL);
|
||||
$this->candidates = $candidates;
|
||||
}
|
||||
|
||||
protected function readParameters(): void
|
||||
{
|
||||
$this->file->rewind();
|
||||
|
||||
while (!$this->file->eof()) {
|
||||
$line = $this->file->fgets();
|
||||
$matches = [];
|
||||
|
||||
if (!isset($this->candidates) && preg_match(self::CANDIDATES_PATTERN, $line, $matches)) {
|
||||
$parse = $matches['candidates'];
|
||||
$parse = CondorcetUtil::prepareParse($parse, false);
|
||||
|
||||
foreach ($parse as &$oneCandidate) {
|
||||
$oneCandidate = new Candidate($oneCandidate);
|
||||
}
|
||||
|
||||
$this->addCandidates($parse);
|
||||
} elseif (!isset($this->numberOfSeats) && preg_match(self::SEATS_PATTERN, $line, $matches)) {
|
||||
$this->numberOfSeats = (int) $matches['seats'];
|
||||
} elseif (!isset($this->implicitRanking) && preg_match(self::IMPLICIT_PATTERN, $line, $matches)) {
|
||||
$parse = mb_strtolower($matches['implicitRanking']);
|
||||
$this->implicitRanking = $this->boolParser($parse);
|
||||
} elseif (!isset($this->voteWeight) && preg_match(self::WEIGHT_PATTERN, $line, $matches)) {
|
||||
$parse = mb_strtolower($matches['weight']);
|
||||
$this->voteWeight = $this->boolParser($parse);
|
||||
} elseif (!empty($line) && !str_starts_with($line, '#')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseCandidatesFromVotes(): void
|
||||
{
|
||||
$this->file->rewind();
|
||||
|
||||
$candidates = [];
|
||||
|
||||
while (!$this->file->eof()) {
|
||||
$line = $this->file->fgets();
|
||||
|
||||
if (!empty($line) && !str_starts_with($line, '#')) {
|
||||
if (($pos = mb_strpos($line, '||')) !== false) {
|
||||
$line = mb_substr($line, ($pos + 2));
|
||||
}
|
||||
|
||||
if (($pos = mb_strpos($line, '||')) !== false) {
|
||||
$line = mb_substr($line, ($pos + 2));
|
||||
}
|
||||
|
||||
foreach (['#', '*', '^'] as $c) {
|
||||
if (($pos = mb_strpos($line, $c)) !== false) {
|
||||
$line = mb_substr($line, 0, $pos);
|
||||
}
|
||||
}
|
||||
|
||||
$line = str_replace('>', '=', $line);
|
||||
$line = explode('=', $line);
|
||||
|
||||
foreach ($line as $oneCandidate) {
|
||||
$oneCandidate = trim($oneCandidate);
|
||||
|
||||
if ($oneCandidate !== self::SPECIAL_KEYWORD_EMPTY_RANKING) {
|
||||
$candidates[$oneCandidate] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->addCandidates(array_keys($candidates));
|
||||
}
|
||||
|
||||
protected function boolParser(string $parse): bool
|
||||
{
|
||||
return match ($parse) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?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\Tools\Converters;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI};
|
||||
use CondorcetPHP\Condorcet\Election;
|
||||
|
||||
interface ConverterInterface
|
||||
{
|
||||
#[PublicAPI]
|
||||
#[Description('Add the tideman data ton an election object')]
|
||||
#[FunctionReturn('The election object')]
|
||||
public function setDataToAnElection(
|
||||
#[FunctionParameter('Add an existing election, useful if you want to set up some parameters or add extra candidates. If null an election object will be created for you.')]
|
||||
Election $election = new Election
|
||||
): Election;
|
||||
}
|
114
include/condorcet/src/Tools/Converters/DavidHillFormat.php
Normal file
114
include/condorcet/src/Tools/Converters/DavidHillFormat.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?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\Tools\Converters;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Candidate, Election};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related};
|
||||
|
||||
class DavidHillFormat implements ConverterInterface
|
||||
{
|
||||
protected array $lines;
|
||||
|
||||
#[PublicAPI]
|
||||
public readonly array $candidates;
|
||||
#[PublicAPI]
|
||||
public readonly int $NumberOfSeats;
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Read a Tideman format file')]
|
||||
public function __construct(
|
||||
#[FunctionParameter('File absolute path')]
|
||||
string $filePath
|
||||
) {
|
||||
$this->lines = file($filePath, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES);
|
||||
(end($this->lines) === '') ? array_pop($this->lines) : null; # Remove bad format from most popular source for this format (elections A01 and A04)
|
||||
|
||||
$this->readNumberOfSeats();
|
||||
$this->readCandidatesNames();
|
||||
$this->readVotes();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Add the data to an election object')]
|
||||
#[FunctionReturn('The election object')]
|
||||
#[Related("Tools\CondorcetElectionFormat::setDataToAnElection", "Tools\DebianFormat::setDataToAnElection")]
|
||||
public function setDataToAnElection(
|
||||
#[FunctionParameter('Add an existing election, useful if you want to set up some parameters or add extra candidates. If null an election object will be created for you.')]
|
||||
?Election $election = null
|
||||
): Election {
|
||||
if ($election === null) {
|
||||
$election = new Election;
|
||||
}
|
||||
|
||||
$election->setNumberOfSeats($this->NumberOfSeats);
|
||||
|
||||
foreach ($this->candidates as $oneCandidate) {
|
||||
$election->addCandidate($oneCandidate);
|
||||
}
|
||||
|
||||
foreach ($this->lines as $oneVote) {
|
||||
$election->addVote($oneVote);
|
||||
}
|
||||
|
||||
return $election;
|
||||
}
|
||||
|
||||
// Internal
|
||||
protected function readNumberOfSeats(): void
|
||||
{
|
||||
$first_line = reset($this->lines);
|
||||
|
||||
$this->NumberOfSeats = (int) explode(' ', $first_line)[1];
|
||||
}
|
||||
|
||||
protected function readCandidatesNames(): void
|
||||
{
|
||||
$last_line = end($this->lines);
|
||||
$last_line = ltrim($last_line, '"');
|
||||
$last_line = explode('" "', $last_line);
|
||||
|
||||
// Remove Election Name
|
||||
array_pop($last_line);
|
||||
|
||||
foreach ($last_line as &$oneCandidate) {
|
||||
$oneCandidate = str_replace('"', '', $oneCandidate);
|
||||
$oneCandidate = new Candidate($oneCandidate);
|
||||
}
|
||||
|
||||
$this->candidates = $last_line;
|
||||
}
|
||||
|
||||
protected function readVotes(): void
|
||||
{
|
||||
// Remove last two lines
|
||||
array_pop($this->lines); // Last
|
||||
array_pop($this->lines); // New Last
|
||||
|
||||
// Remove first line
|
||||
array_shift($this->lines);
|
||||
|
||||
// Read each line
|
||||
foreach ($this->lines as &$oneVote) {
|
||||
$oneVote = explode(' ', $oneVote);
|
||||
|
||||
// Remove first line
|
||||
array_shift($oneVote);
|
||||
// Remove Last line
|
||||
array_pop($oneVote);
|
||||
|
||||
foreach ($oneVote as &$oneRank) {
|
||||
$oneRank = (int) $oneRank;
|
||||
$oneRank = $this->candidates[$oneRank - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
include/condorcet/src/Tools/Converters/DebianFormat.php
Normal file
114
include/condorcet/src/Tools/Converters/DebianFormat.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?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\Tools\Converters;
|
||||
|
||||
use CondorcetPHP\Condorcet\{Candidate, Election, Vote};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI, Related};
|
||||
|
||||
class DebianFormat implements ConverterInterface
|
||||
{
|
||||
protected array $lines;
|
||||
|
||||
#[PublicAPI]
|
||||
public readonly array $candidates;
|
||||
#[PublicAPI]
|
||||
public readonly array $votes;
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Read a Tideman format file')]
|
||||
public function __construct(
|
||||
#[FunctionParameter('File absolute path')]
|
||||
string $filePath
|
||||
) {
|
||||
$this->lines = file($filePath, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
$this->readCandidatesNames();
|
||||
$this->readVotes();
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Add the Debian data to an election object')]
|
||||
#[FunctionReturn('The election object')]
|
||||
#[Related("Tools\CondorcetElectionFormat::setDataToAnElection", "Tools\DavidHillFormat::setDataToAnElection")]
|
||||
public function setDataToAnElection(
|
||||
#[FunctionParameter('Add an existing election, useful if you want to set up some parameters or add extra candidates. If null an election object will be created for you.')]
|
||||
?Election $election = null
|
||||
): Election {
|
||||
if ($election === null) {
|
||||
$election = new Election;
|
||||
$election->setNumberOfSeats(1);
|
||||
}
|
||||
|
||||
foreach ($this->candidates as $oneCandidate) {
|
||||
$election->addCandidate($oneCandidate);
|
||||
}
|
||||
|
||||
foreach ($this->votes as $oneVote) {
|
||||
$election->addVote($oneVote);
|
||||
}
|
||||
|
||||
return $election;
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
protected function readCandidatesNames(): void
|
||||
{
|
||||
$pattern = '/^.*Option [1-9].*: (?<candidateName>.*)$/m';
|
||||
|
||||
$candidatesLines = preg_grep($pattern, $this->lines);
|
||||
|
||||
$candidates = [];
|
||||
// $k = 1;
|
||||
|
||||
foreach ($candidatesLines as $oneCandidateLine) {
|
||||
$match = null;
|
||||
preg_match($pattern, $oneCandidateLine, $match);
|
||||
|
||||
// $candidates[$k++] = new Candidate( \trim($match['candidateName']) );
|
||||
// $candidates[] = new Candidate( \trim( $match['candidateName']) );
|
||||
$candidateName = $match['candidateName'];
|
||||
mb_check_encoding($candidateName) || ($candidateName = mb_convert_encoding($candidateName, 'UTF-8', 'ISO-8859-16'));
|
||||
$candidateName = trim($candidateName);
|
||||
|
||||
$candidates[] = new Candidate($candidateName);
|
||||
}
|
||||
|
||||
$this->candidates = $candidates;
|
||||
}
|
||||
|
||||
protected function readVotes(): void
|
||||
{
|
||||
$pattern = '/^(V:)? ?(?<Ranking>[1-9-]+)[ \t]+(?<MD5>[a-z0-9]+)$/m';
|
||||
|
||||
$votesLines = preg_grep($pattern, $this->lines);
|
||||
$votes = [];
|
||||
|
||||
foreach ($votesLines as $oneVoteLine) {
|
||||
$match = null;
|
||||
|
||||
preg_match($pattern, trim($oneVoteLine), $match);
|
||||
$oneVoteLineRanking = mb_str_split($match['Ranking']);
|
||||
|
||||
$oneVote = [];
|
||||
foreach ($oneVoteLineRanking as $candidateKey => $candidateRankEvaluation) {
|
||||
if (is_numeric($candidateRankEvaluation)) {
|
||||
$oneVote[(int) $candidateRankEvaluation][] = $this->candidates[$candidateKey];
|
||||
}
|
||||
}
|
||||
|
||||
$votes[] = new Vote($oneVote, $match['MD5']);
|
||||
}
|
||||
|
||||
$this->votes = $votes;
|
||||
}
|
||||
}
|
109
include/condorcet/src/Utils/CondorcetUtil.php
Normal file
109
include/condorcet/src/Utils/CondorcetUtil.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?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\Utils;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, FunctionParameter, FunctionReturn, PublicAPI};
|
||||
use CondorcetPHP\Condorcet\Throwable\FileDoesNotExistException;
|
||||
use CondorcetPHP\Condorcet\{Candidate, Result, Vote};
|
||||
|
||||
abstract class CondorcetUtil
|
||||
{
|
||||
// Check JSON format
|
||||
public static function isValidJsonForCondorcet(string $string): void
|
||||
{
|
||||
if (is_numeric($string) || $string === 'true' || $string === 'false' || $string === 'null' || $string === '{}' || empty($string)) {
|
||||
throw new \JsonException;
|
||||
}
|
||||
}
|
||||
|
||||
public static function prepareJson(string $input): mixed
|
||||
{
|
||||
self::isValidJsonForCondorcet($input);
|
||||
|
||||
return json_decode(json: $input, associative: true, flags: \JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
// Generic action before parsing data from string input
|
||||
public static function prepareParse(string $input, bool $isFile): array
|
||||
{
|
||||
// Is string or is file ?
|
||||
if ($isFile === true) {
|
||||
if (!is_file($input)) {
|
||||
throw new FileDoesNotExistException('Specified input file does not exist. path: '.$input);
|
||||
}
|
||||
|
||||
$input = file_get_contents($input);
|
||||
}
|
||||
|
||||
// Line
|
||||
$input = preg_replace("(\r\n|\n|\r)", ';', $input);
|
||||
$input = explode(';', $input);
|
||||
|
||||
// Delete comments
|
||||
foreach ($input as $key => &$line) {
|
||||
// Delete comments
|
||||
$is_comment = mb_strpos($line, '#');
|
||||
if ($is_comment !== false) {
|
||||
$line = mb_substr($line, 0, $is_comment);
|
||||
}
|
||||
|
||||
// Trim
|
||||
$line = trim($line);
|
||||
|
||||
// Remove empty
|
||||
if (empty($line)) {
|
||||
unset($input[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
// Simplify Condorcet Var_Dump. Transform object to String.
|
||||
#[PublicAPI]
|
||||
#[Description("Provide pretty re-formatting, human compliant, of all Condorcet PHP object or result set.\nCan be use before a var_dump, or just to get more simple data output.")]
|
||||
#[FunctionReturn('New formatted data.')]
|
||||
public static function format(
|
||||
#[FunctionParameter('Input to convert')]
|
||||
mixed $input,
|
||||
#[FunctionParameter('If true. Will convert Candidate objects into string representation of their name')]
|
||||
bool $convertObject = true
|
||||
): mixed {
|
||||
if (\is_object($input)) {
|
||||
$r = $input;
|
||||
|
||||
if ($convertObject) {
|
||||
if ($input instanceof Candidate) {
|
||||
$r = (string) $input;
|
||||
} elseif ($input instanceof Vote) {
|
||||
$r = $input->getSimpleRanking();
|
||||
} elseif ($input instanceof Result) {
|
||||
$r = $input->getResultAsArray(true);
|
||||
}
|
||||
}
|
||||
} elseif (!\is_array($input)) {
|
||||
$r = $input;
|
||||
} else {
|
||||
foreach ($input as $key => $line) {
|
||||
$input[$key] = self::format($line, $convertObject);
|
||||
}
|
||||
|
||||
if (\count($input) === 1 && \is_int(key($input)) && (!\is_array(reset($input)) || \count(reset($input)) === 1)) {
|
||||
$r = reset($input);
|
||||
} else {
|
||||
$r = $input;
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
}
|
142
include/condorcet/src/Utils/VoteEntryParser.php
Normal file
142
include/condorcet/src/Utils/VoteEntryParser.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?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\Utils;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\Throws;
|
||||
use CondorcetPHP\Condorcet\Throwable\VoteInvalidFormatException;
|
||||
use CondorcetPHP\Condorcet\Tools\Converters\CondorcetElectionFormat;
|
||||
|
||||
// Base Condorcet class
|
||||
class VoteEntryParser
|
||||
{
|
||||
public readonly string $originalEntry;
|
||||
|
||||
public readonly ?string $comment;
|
||||
public readonly int $multiple;
|
||||
public readonly ?array $ranking;
|
||||
public readonly ?array $tags;
|
||||
public readonly int $weight;
|
||||
|
||||
public function __construct(string $entry)
|
||||
{
|
||||
$this->originalEntry = $entry;
|
||||
|
||||
// Disallow < and "
|
||||
if (preg_match('/<|"/mi', $entry) === 1) {
|
||||
throw new VoteInvalidFormatException("found '<' or '|' in " . $entry);
|
||||
}
|
||||
|
||||
$this->comment = $this->getComment($entry, true);
|
||||
|
||||
$this->multiple = self::parseIntValueFromVoteStringOffset('*', $entry, true);
|
||||
$this->weight = self::parseIntValueFromVoteStringOffset('^', $entry, true);
|
||||
|
||||
$this->tags = $this->convertTagsFromVoteString($entry, true);
|
||||
|
||||
$this->ranking = self::convertRankingFromString($entry);
|
||||
}
|
||||
|
||||
// From a string like 'A>B=C=H>G=T>Q'
|
||||
public static function convertRankingFromString(string $formula): ?array
|
||||
{
|
||||
$formula = trim($formula);
|
||||
|
||||
// Condorcet Election Format special string
|
||||
if (empty($formula)) {
|
||||
return null;
|
||||
} elseif ($formula === CondorcetElectionFormat::SPECIAL_KEYWORD_EMPTY_RANKING) {
|
||||
return [];
|
||||
} else {
|
||||
$ranking = explode('>', $formula);
|
||||
|
||||
foreach ($ranking as &$rank_vote) {
|
||||
$rank_vote = explode('=', $rank_vote);
|
||||
$rank_vote = array_filter($rank_vote);
|
||||
|
||||
// Del space at start and end
|
||||
foreach ($rank_vote as &$value) {
|
||||
$value = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
return array_filter($ranking);
|
||||
}
|
||||
}
|
||||
|
||||
public static function convertTagsFromVoteString(string &$voteString, bool $cut = false): ?array
|
||||
{
|
||||
$offset = mb_strpos($voteString, '||');
|
||||
|
||||
if (\is_int($offset)) {
|
||||
$tagsPart = mb_substr($voteString, 0, $offset);
|
||||
|
||||
$tags = explode(',', $tagsPart);
|
||||
|
||||
array_walk($tags, static function (string &$value): void {
|
||||
$value = trim($value);
|
||||
});
|
||||
|
||||
$cut && $voteString = mb_substr($voteString, $offset + 2);
|
||||
|
||||
return $tags;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getComment(string &$voteString, bool $cut = false): ?string
|
||||
{
|
||||
$offset = mb_strpos($voteString, '#');
|
||||
|
||||
if (\is_int($offset)) {
|
||||
$comment = trim(mb_substr($voteString, $offset + 1));
|
||||
|
||||
$cut && $voteString = mb_substr($voteString, 0, $offset);
|
||||
|
||||
return $comment;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
public static function parseIntValueFromVoteStringOffset(string $character, string &$entry, bool $cut = false): int
|
||||
{
|
||||
$offset = mb_strpos($entry, $character);
|
||||
|
||||
if (\is_int($offset)) {
|
||||
$input = trim(mb_substr($entry, $offset + 1));
|
||||
|
||||
$value = '';
|
||||
|
||||
foreach (mb_str_split($input) as $char) {
|
||||
if (!\in_array($char, ['#', '^', '*', "\n"], true)) {
|
||||
$value .= $char;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Errors
|
||||
if (!is_numeric($value)) {
|
||||
throw new VoteInvalidFormatException("the value '{$value}' is not an integer.");
|
||||
}
|
||||
|
||||
$cut && $entry = mb_substr($entry, 0, $offset);
|
||||
|
||||
$value = \intval($value);
|
||||
return ($value > 0) ? $value : 1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
57
include/condorcet/src/Utils/VoteUtil.php
Normal file
57
include/condorcet/src/Utils/VoteUtil.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?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\Utils;
|
||||
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\Throws;
|
||||
use CondorcetPHP\Condorcet\Throwable\VoteInvalidFormatException;
|
||||
|
||||
// Base Condorcet class
|
||||
abstract class VoteUtil
|
||||
{
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
public static function tagsConvert(array|string|null $tags): ?array
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return null;
|
||||
} elseif (\is_string($tags)) {
|
||||
$tags = explode(',', $tags);
|
||||
} else {
|
||||
foreach ($tags as $tag) {
|
||||
if (!\is_string($tag)) {
|
||||
throw new VoteInvalidFormatException('every tag must be of type string, ' . \gettype($tag) . ' given');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tags = array_map(static fn (string $x): string => trim($x), $tags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (empty($tag)) {
|
||||
throw new VoteInvalidFormatException('found empty tag');
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
public static function getRankingAsString(array $ranking): string
|
||||
{
|
||||
foreach ($ranking as &$rank) {
|
||||
if (\is_array($rank)) {
|
||||
sort($rank);
|
||||
$rank = implode(' = ', $rank);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' > ', $ranking);
|
||||
}
|
||||
}
|
680
include/condorcet/src/Vote.php
Normal file
680
include/condorcet/src/Vote.php
Normal file
@ -0,0 +1,680 @@
|
||||
<?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;
|
||||
|
||||
use CondorcetPHP\Condorcet\Throwable\{CandidateDoesNotExistException, VoteInvalidFormatException, VoteNotLinkedException};
|
||||
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, InternalModulesAPI, PublicAPI, Related, Throws};
|
||||
use CondorcetPHP\Condorcet\Utils\{CondorcetUtil, VoteEntryParser, VoteUtil};
|
||||
|
||||
class Vote implements \Iterator, \Stringable
|
||||
{
|
||||
use Linkable;
|
||||
use CondorcetVersion;
|
||||
|
||||
// Implement Iterator
|
||||
|
||||
private int $position = 1;
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 1;
|
||||
}
|
||||
|
||||
public function current(): array
|
||||
{
|
||||
return $this->getRanking()[$this->position];
|
||||
}
|
||||
|
||||
public function key(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->getRanking()[$this->position]);
|
||||
}
|
||||
|
||||
// Vote
|
||||
|
||||
private array $ranking;
|
||||
|
||||
private float $lastTimestamp;
|
||||
|
||||
private int $counter;
|
||||
|
||||
private array $ranking_history = [];
|
||||
|
||||
private int $weight = 1;
|
||||
|
||||
private array $tags = [];
|
||||
|
||||
private string $hashCode = '';
|
||||
|
||||
private ?Election $electionContext = null;
|
||||
|
||||
public bool $notUpdate = false;
|
||||
|
||||
|
||||
// Performance (for internal use)
|
||||
protected static ?\stdClass $cacheKey = null;
|
||||
protected ?\WeakMap $cacheMap = null;
|
||||
|
||||
public static function initCache(): \stdClass
|
||||
{
|
||||
self::$cacheKey = new \stdClass;
|
||||
return self::$cacheKey;
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$cacheKey = null;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Build a vote object.')]
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Vote::setRanking', 'Vote::addTags')]
|
||||
public function __construct(
|
||||
#[FunctionParameter('Equivalent to Vote::setRanking method')]
|
||||
array|string $ranking,
|
||||
#[FunctionParameter('Equivalent to Vote::addTags method')]
|
||||
array|string|null $tags = null,
|
||||
#[FunctionParameter('Set your own timestamp metadata on Ranking')]
|
||||
?float $ownTimestamp = null,
|
||||
#[FunctionParameter('Try to convert directly your candidates from sting input" to Candidate object of one election')]
|
||||
?Election $electionContext = null
|
||||
) {
|
||||
$this->cacheMap = new \WeakMap;
|
||||
|
||||
$this->electionContext = $electionContext;
|
||||
$tagsFromString = null;
|
||||
|
||||
// Vote Weight
|
||||
if (\is_string($ranking)) {
|
||||
$parsedVote = new VoteEntryParser($ranking);
|
||||
|
||||
if ($parsedVote->weight > 1) {
|
||||
$weight = $parsedVote->weight;
|
||||
}
|
||||
|
||||
$tagsFromString = $parsedVote->tags;
|
||||
|
||||
$ranking = $parsedVote->ranking ?? [];
|
||||
}
|
||||
|
||||
$this->setRanking($ranking, $ownTimestamp);
|
||||
$tags === null || $this->addTags($tags);
|
||||
$tagsFromString === null || $this->addTags($tagsFromString);
|
||||
|
||||
if (isset($weight)) {
|
||||
$this->setWeight($weight);
|
||||
}
|
||||
|
||||
$this->electionContext = null;
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$this->position = 1;
|
||||
$this->link = null;
|
||||
|
||||
$var = get_object_vars($this);
|
||||
unset($var['cacheMap']);
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
public function __wakeup(): void
|
||||
{
|
||||
$this->cacheMap = new \WeakMap;
|
||||
}
|
||||
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->destroyAllLink();
|
||||
$this->computeHashCode();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if (empty($this->getTags())) {
|
||||
return $this->getSimpleRanking();
|
||||
} else {
|
||||
return $this->getTagsAsString().' || '.$this->getSimpleRanking();
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get Object hash (cryptographic)')]
|
||||
#[FunctionReturn('SHA hash code.')]
|
||||
#[Related('Vote::getWeight')]
|
||||
public function getHashCode(): string
|
||||
{
|
||||
return $this->hashCode;
|
||||
}
|
||||
|
||||
// -------
|
||||
|
||||
// GETTERS
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the actual Ranking of this Vote.')]
|
||||
#[FunctionReturn('Multidimenssionnal array populated by Candidate object.')]
|
||||
#[Related('Vote::setRanking')]
|
||||
public function getRanking(
|
||||
#[FunctionParameter('Sort Candidate in a Rank by name. Useful for performant internal calls from methods.')]
|
||||
bool $sortCandidatesInRank = true
|
||||
): array {
|
||||
$r = $this->ranking;
|
||||
|
||||
foreach ($r as &$oneRank) {
|
||||
if ($sortCandidatesInRank && \count($oneRank) > 1) {
|
||||
sort($oneRank, \SORT_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return an history of each vote change, with timestamp.')]
|
||||
#[FunctionReturn('An explicit multi-dimenssional array.')]
|
||||
#[Related('Vote::getCreateTimestamp')]
|
||||
public function getHistory(): array
|
||||
{
|
||||
return $this->ranking_history;
|
||||
}
|
||||
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the registered tags for this Vote.')]
|
||||
#[FunctionReturn('List of registered tag.')]
|
||||
#[Related('Vote::getTagsAsString', 'Vote::addTags', 'Vote::removeTags')]
|
||||
public function getTags(): array
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the registered tags for this Vote.')]
|
||||
#[FunctionReturn('List of registered tag as string separated by commas.')]
|
||||
#[Related('Vote::getTags', 'Vote::addTags', 'Vote::removeTags')]
|
||||
public function getTagsAsString(): string
|
||||
{
|
||||
return implode(',', $this->getTags());
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the timestamp corresponding of the creation of this vote.')]
|
||||
#[FunctionReturn('Timestamp')]
|
||||
#[Related('Candidate::getTimestamp')]
|
||||
public function getCreateTimestamp(): float
|
||||
{
|
||||
return $this->ranking_history[0]['timestamp'];
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the timestamp corresponding of the last vote change.')]
|
||||
#[FunctionReturn('Timestamp')]
|
||||
#[Related('Vote::getCreateTimestamp')]
|
||||
public function getTimestamp(): float
|
||||
{
|
||||
return $this->lastTimestamp;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Count the number of candidate provide into the active Ranking set.')]
|
||||
#[FunctionReturn('Number of Candidate into ranking.')]
|
||||
public function countRankingCandidates(): int
|
||||
{
|
||||
return $this->counter;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get all the candidates object set in the last ranking of this Vote.')]
|
||||
#[FunctionReturn('Candidates list.')]
|
||||
#[Related('Vote::getRanking', 'Vote::countRankingCandidates')]
|
||||
public function getAllCandidates(): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($this->getRanking(false) as $rank) {
|
||||
foreach ($rank as $oneCandidate) {
|
||||
$list[] = $oneCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the vote actual ranking complete for the contexte of the provide election. Election must be linked to the Vote object.')]
|
||||
#[FunctionReturn('Contextual full ranking.')]
|
||||
#[Throws(VoteNotLinkedException::class)]
|
||||
#[Related('Vote::getContextualRankingAsString', 'Vote::getRanking')]
|
||||
public function getContextualRanking(
|
||||
#[FunctionParameter('An election already linked to the Vote')]
|
||||
Election $election,
|
||||
): array {
|
||||
return $this->computeContextualRanking($election, true);
|
||||
}
|
||||
|
||||
// Performances
|
||||
#[InternalModulesAPI]
|
||||
public function getContextualRankingWithoutSort(
|
||||
#[FunctionParameter('An election already linked to the Vote')]
|
||||
Election $election,
|
||||
): array {
|
||||
return $this->computeContextualRanking($election, false);
|
||||
}
|
||||
|
||||
protected function computeContextualRanking(
|
||||
#[FunctionParameter('An election already linked to the Vote')]
|
||||
Election $election,
|
||||
#[FunctionParameter('If false, performance can be increased for Implicit Ranking election.')]
|
||||
bool $sortLastRankByName
|
||||
): array {
|
||||
// Cache for internal use
|
||||
if (self::$cacheKey !== null && !$sortLastRankByName && $this->cacheMap->offsetExists(self::$cacheKey)) {
|
||||
return $this->cacheMap->offsetGet(self::$cacheKey);
|
||||
}
|
||||
|
||||
// Normal procedure
|
||||
if (!$this->haveLink($election)) {
|
||||
throw new VoteNotLinkedException;
|
||||
}
|
||||
|
||||
$countContextualCandidate = 0;
|
||||
|
||||
$present = $this->getAllCandidates();
|
||||
$candidates_list = $election->getCandidatesList();
|
||||
$candidates_count = $election->countCandidates();
|
||||
|
||||
$newRanking = $this->computeContextualRankingWithoutImplicit($this->getRanking(false), $election, $countContextualCandidate);
|
||||
|
||||
if ($election->getImplicitRankingRule() && $countContextualCandidate < $candidates_count) {
|
||||
$last_rank = [];
|
||||
$needed = $candidates_count - $countContextualCandidate;
|
||||
|
||||
foreach ($candidates_list as $oneCandidate) {
|
||||
if (!\in_array(needle: $oneCandidate, haystack: $present, strict: true)) {
|
||||
$last_rank[] = $oneCandidate;
|
||||
}
|
||||
|
||||
if (\count($last_rank) === $needed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sortLastRankByName) {
|
||||
sort($last_rank, \SORT_STRING);
|
||||
}
|
||||
|
||||
$newRanking[\count($newRanking) + 1] = $last_rank;
|
||||
}
|
||||
|
||||
// Cache for internal use
|
||||
if (self::$cacheKey !== null && !$sortLastRankByName) {
|
||||
$this->cacheMap->offsetSet(self::$cacheKey, $newRanking);
|
||||
}
|
||||
|
||||
return $newRanking;
|
||||
}
|
||||
|
||||
protected function computeContextualRankingWithoutImplicit(array $ranking, Election $election, int &$countContextualCandidate = 0): array
|
||||
{
|
||||
$newRanking = [];
|
||||
$nextRank = 1;
|
||||
$rankChange = false;
|
||||
|
||||
foreach ($ranking as $CandidatesInRanks) {
|
||||
foreach ($CandidatesInRanks as $candidate) {
|
||||
if ($election->isRegisteredCandidate($candidate, true)) {
|
||||
$newRanking[$nextRank][] = $candidate;
|
||||
$countContextualCandidate++;
|
||||
$rankChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($rankChange) {
|
||||
$nextRank++;
|
||||
$rankChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $newRanking;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Return the vote actual ranking complete for the contexte of the provide election. Election must be linked to the Vote object.')]
|
||||
#[FunctionReturn('Contextual full ranking, with string instead Candidate object.')]
|
||||
#[Related('Vote::getContextualRanking', 'Vote::getRanking')]
|
||||
public function getContextualRankingAsString(
|
||||
#[FunctionParameter('An election already linked to the Vote')]
|
||||
Election $election
|
||||
): array {
|
||||
return CondorcetUtil::format($this->getContextualRanking($election), true);
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the current ranking as a string format. Optionally with an election context, see Election::getContextualRanking()')]
|
||||
#[FunctionReturn("String like 'A>D=C>B'")]
|
||||
#[Related('Vote::getRanking')]
|
||||
public function getSimpleRanking(
|
||||
#[FunctionParameter('An election already linked to the Vote')]
|
||||
?Election $context = null,
|
||||
#[FunctionParameter('Include or not the weight symbol and value')]
|
||||
bool $displayWeight = true
|
||||
): string {
|
||||
$ranking = $context ? $this->getContextualRanking($context) : $this->getRanking();
|
||||
|
||||
$simpleRanking = VoteUtil::getRankingAsString($ranking);
|
||||
|
||||
if ($displayWeight && $this->weight > 1 && (($context && $context->isVoteWeightAllowed()) || $context === null)) {
|
||||
$simpleRanking .= ' ^'.$this->getWeight();
|
||||
}
|
||||
|
||||
return $simpleRanking;
|
||||
}
|
||||
|
||||
|
||||
// SETTERS
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description("Set a new ranking for this vote.\n\nNote that if your vote is already linked to one ore more elections, your ranking must be compliant with all of them, else an exception is throw. For do this, you need to use only valid Candidate object, you can't register a new ranking from string if your vote is already linked to an election.")]
|
||||
#[FunctionReturn('In case of success, return TRUE')]
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
#[Example('Manual - Add a vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Vote::getRanking', 'Vote::getHistory', 'Vote::__construct')]
|
||||
public function setRanking(
|
||||
#[FunctionParameter('A Ranking. Have a look at the Wiki https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote to learn the available ranking formats.')]
|
||||
array|string $ranking,
|
||||
#[FunctionParameter('Set your own timestamp metadata on Ranking. Your timestamp must be > than last registered timestamp. Else, an exception will be throw.')]
|
||||
?float $ownTimestamp = null
|
||||
): bool {
|
||||
// Timestamp
|
||||
if ($ownTimestamp !== null) {
|
||||
if (!empty($this->ranking_history) && $this->getTimestamp() >= $ownTimestamp) {
|
||||
throw new VoteInvalidFormatException('Timestamp format of vote is not correct');
|
||||
}
|
||||
}
|
||||
|
||||
// Ranking
|
||||
$candidateCounter = $this->formatRanking($ranking);
|
||||
|
||||
if ($this->electionContext !== null) {
|
||||
$this->electionContext->convertRankingCandidates($ranking);
|
||||
}
|
||||
|
||||
if (!$this->notUpdate) {
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
$link->prepareUpdateVote($this);
|
||||
}
|
||||
}
|
||||
|
||||
$this->ranking = $ranking;
|
||||
$this->lastTimestamp = $ownTimestamp ?? microtime(true);
|
||||
$this->counter = $candidateCounter;
|
||||
|
||||
$this->archiveRanking();
|
||||
|
||||
if (\count($this->link) > 0) {
|
||||
try {
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
if (!$link->checkVoteCandidate($this)) {
|
||||
throw new VoteInvalidFormatException('vote does not match candidate in this election');
|
||||
}
|
||||
}
|
||||
} catch (VoteInvalidFormatException $e) {
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
$link->setStateToVote();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (!$this->notUpdate) {
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
$link->finishUpdateVote($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->computeHashCode();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function formatRanking(array|string &$ranking): int
|
||||
{
|
||||
if (\is_string($ranking)) {
|
||||
$ranking = (new VoteEntryParser($ranking))->ranking ?? [];
|
||||
}
|
||||
|
||||
$ranking = array_filter($ranking, static fn ($key): bool => is_numeric($key), \ARRAY_FILTER_USE_KEY);
|
||||
|
||||
ksort($ranking);
|
||||
|
||||
$i = 1;
|
||||
$vote_r = [];
|
||||
foreach ($ranking as &$value) {
|
||||
if (!\is_array($value)) {
|
||||
$vote_r[$i] = [$value];
|
||||
} else {
|
||||
$vote_r[$i] = $value;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
$ranking = $vote_r;
|
||||
|
||||
$counter = 0;
|
||||
$list_candidate = [];
|
||||
foreach ($ranking as &$line) {
|
||||
foreach ($line as &$Candidate) {
|
||||
if (!($Candidate instanceof Candidate)) {
|
||||
$Candidate = new Candidate($Candidate);
|
||||
$Candidate->setProvisionalState(true);
|
||||
}
|
||||
|
||||
$counter++;
|
||||
|
||||
// Check Duplicate
|
||||
|
||||
// Check objet reference AND check candidates name
|
||||
if (!\in_array($name = $Candidate->getName(), $list_candidate, true)) {
|
||||
$list_candidate[] = $name;
|
||||
} else {
|
||||
throw new VoteInvalidFormatException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Remove candidate from ranking. Set a new ranking and archive the old ranking.')]
|
||||
#[FunctionReturn('True on success.')]
|
||||
#[Throws(CandidateDoesNotExistException::class)]
|
||||
#[Related('Vote::setRanking')]
|
||||
public function removeCandidate(
|
||||
#[FunctionParameter('Candidate object or string')]
|
||||
Candidate|string $candidate
|
||||
): bool {
|
||||
if ($candidate instanceof Candidate) {
|
||||
$strict = true;
|
||||
} else {
|
||||
$strict = false;
|
||||
}
|
||||
|
||||
$ranking = $this->getRanking();
|
||||
|
||||
$rankingCandidate = $this->getAllCandidates();
|
||||
|
||||
if (!\in_array(needle: $candidate, haystack: $rankingCandidate, strict: $strict)) {
|
||||
throw new CandidateDoesNotExistException((string) $candidate);
|
||||
}
|
||||
|
||||
foreach ($ranking as $rankingKey => &$rank) {
|
||||
foreach ($rank as $oneRankKey => $oneRankValue) {
|
||||
if ($strict ? $oneRankValue === $candidate : $oneRankValue == $candidate) {
|
||||
unset($rank[$oneRankKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($rank)) {
|
||||
unset($ranking[$rankingKey]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRanking($ranking);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Add tag(s) on this Vote.')]
|
||||
#[FunctionReturn('In case of success, return TRUE')]
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Vote::removeTags')]
|
||||
public function addTags(
|
||||
#[FunctionParameter('Tag(s) are non-numeric alphanumeric string. They can be added by string separated by commas or an array.')]
|
||||
array|string $tags
|
||||
): bool {
|
||||
$tags = VoteUtil::tagsConvert($tags) ?? [];
|
||||
|
||||
foreach ($tags as $key => $tag) {
|
||||
if (\in_array(needle: $tag, haystack: $this->tags, strict: true)) {
|
||||
unset($tags[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$this->tags[] = $tag;
|
||||
}
|
||||
|
||||
$this->computeHashCode();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Remove registered tag(s) on this Vote.')]
|
||||
#[FunctionReturn('List of deleted tags.')]
|
||||
#[Example('Manual - Add Vote', 'https://github.com/julien-boudry/Condorcet/wiki/II-%23-B.-Vote-management-%23-1.-Add-Vote')]
|
||||
#[Related('Vote::addTags')]
|
||||
public function removeTags(
|
||||
#[FunctionParameter('They can be added by string separated by commas or an array.')]
|
||||
array|string $tags
|
||||
): array {
|
||||
$tags = VoteUtil::tagsConvert($tags);
|
||||
|
||||
if (empty($tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rm = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$tagK = array_search(needle: $tag, haystack: $this->tags, strict: true);
|
||||
|
||||
if ($tagK === false) {
|
||||
unset($tags[$key]);
|
||||
} else {
|
||||
$rm[] = $this->tags[$tagK];
|
||||
unset($this->tags[$tagK]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->computeHashCode();
|
||||
return $rm;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Remove all registered tag(s) on this Vote.')]
|
||||
#[FunctionReturn('Return True.')]
|
||||
#[Related('Vote::addTags', 'Vote::removeTags')]
|
||||
public function removeAllTags(): bool
|
||||
{
|
||||
$this->removeTags($this->getTags());
|
||||
return true;
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Get the vote weight. The vote weight capacity must be active at the election level for producing effect on the result.')]
|
||||
#[FunctionReturn('Weight. Default weight is 1.')]
|
||||
#[Related('Vote::setWeight')]
|
||||
public function getWeight(
|
||||
#[FunctionParameter('In the context of wich election? (optional)')]
|
||||
?Election $context = null
|
||||
): int {
|
||||
if ($context !== null && !$context->isVoteWeightAllowed()) {
|
||||
return 1;
|
||||
} else {
|
||||
return $this->weight;
|
||||
}
|
||||
}
|
||||
|
||||
#[PublicAPI]
|
||||
#[Description('Set a vote weight. The vote weight capacity must be active at the election level for producing effect on the result.')]
|
||||
#[FunctionReturn('New weight.')]
|
||||
#[Throws(VoteInvalidFormatException::class)]
|
||||
#[Related('Vote::getWeight')]
|
||||
public function setWeight(
|
||||
#[FunctionParameter('The new vote weight.')]
|
||||
int $newWeight
|
||||
): int {
|
||||
if ($newWeight < 1) {
|
||||
throw new VoteInvalidFormatException('the vote weight can not be less than 1');
|
||||
}
|
||||
|
||||
if ($newWeight !== $this->weight) {
|
||||
$this->weight = $newWeight;
|
||||
|
||||
if (\count($this->link) > 0) {
|
||||
foreach ($this->getLinks() as $link => $value) {
|
||||
$link->setStateToVote();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->computeHashCode();
|
||||
|
||||
return $this->getWeight();
|
||||
}
|
||||
|
||||
/////////// INTERNAL ///////////
|
||||
|
||||
private function archiveRanking(): void
|
||||
{
|
||||
$this->ranking_history[] = ['ranking' => $this->ranking, 'timestamp' => $this->lastTimestamp, 'counter' => $this->counter];
|
||||
|
||||
$this->rewind();
|
||||
}
|
||||
|
||||
private function computeHashCode(): string
|
||||
{
|
||||
return $this->hashCode = hash('sha224', ((string) $this) . microtime(false));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user