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

View File

@ -0,0 +1,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
);
}
}

View 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;
}

View 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);
}
}

View 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));
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,172 @@
<?php
/*
Part of KEMENYYOUNG 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 = ['KemenyYoung', '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);
}
}

View File

@ -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', 'HareNiemeyer 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);
}
}

View File

@ -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;
}

View 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;
}
}

View File

@ -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);
}
}

View 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);
}
}

View 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 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);
}
}

View 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 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);
}
}

View File

@ -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;
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View 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];
}
}

View 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);
}
}
}

View 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 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];
}
}

View 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);
}
}

View 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;
}
}
}
}

View 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
}

View 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));
}
}
}
}

View 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;
}
}

View 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;
}
}
}
}
}

View 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),
};
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,97 @@
<?php
/*
Condorcet PHP - Election manager and results calculator.
Designed for the Condorcet method. Integrating a large number of algorithms extending Condorcet. Expandable for all types of voting systems.
By Julien Boudry and contributors - MIT LICENSE (Please read LICENSE.txt)
https://github.com/julien-boudry/Condorcet
*/
declare(strict_types=1);
namespace CondorcetPHP\Condorcet\Console;
use Symfony\Component\Console\Application as SymfonyConsoleApplication;
use CondorcetPHP\Condorcet\Condorcet;
use CondorcetPHP\Condorcet\Console\Commands\ElectionCommand;
use CondorcetPHP\Condorcet\Throwable\Internal\NoGitShellException;
abstract class CondorcetApplication
{
public static SymfonyConsoleApplication $SymfonyConsoleApplication;
/**
* @infection-ignore-all
*/
public static function run(): void
{
// Run
self::create() && self::$SymfonyConsoleApplication->run();
}
public static function create(): bool
{
// New App
self::$SymfonyConsoleApplication = new SymfonyConsoleApplication('Condorcet', Condorcet::getVersion());
// Election command
$command = new ElectionCommand;
self::$SymfonyConsoleApplication->add($command);
self::$SymfonyConsoleApplication->setDefaultCommand($command->getName(), false);
return true;
}
public static function getVersionWithGitParsing(): string
{
$git = static function (string $path): string {
if (!is_dir($path . \DIRECTORY_SEPARATOR . '.git')) {
throw new NoGitShellException('Path is not valid');
}
$process = proc_open(
'git describe --tags --match="v[0-9]*\.[0-9]*\.[0-9]*"',
[
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes,
$path
);
if (!\is_resource($process)) {
throw new NoGitShellException;
}
$result = trim(stream_get_contents($pipes[1]));
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
if ($returnCode !== 0) {
throw new NoGitShellException;
}
return $result;
};
$applicationOfficialVersion = Condorcet::getVersion();
try {
$version = $git(__DIR__.'/../../');
$commit = explode('-', $version)[2];
$match = [];
preg_match('/^v([0-9]+\.[0-9]+\.[0-9]+)/', $version, $match);
$gitLastestTag = $match[1];
$version = (version_compare($gitLastestTag, $applicationOfficialVersion, '>=')) ? $version : $applicationOfficialVersion.'-(dev)-'.$commit;
} catch (NoGitShellException) { // Git no available, use the Condorcet Version
$version = $applicationOfficialVersion;
}
return $version;
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
Condorcet PHP - Election manager and results calculator.
Designed for the Condorcet method. Integrating a large number of algorithms extending Condorcet. Expandable for all types of voting systems.
By Julien Boudry and contributors - MIT LICENSE (Please read LICENSE.txt)
https://github.com/julien-boudry/Condorcet
*/
declare(strict_types=1);
namespace CondorcetPHP\Condorcet\Console\Helper;
use CondorcetPHP\Condorcet\{Condorcet, Election, Result};
abstract class CommandInputHelper
{
public static function getFilePath(string $path): ?string
{
if (self::pathIsAbsolute($path) && is_file($path)) {
return $path;
} else {
return (is_file($file = getcwd().\DIRECTORY_SEPARATOR.$path)) ? $file : null;
}
}
public static function pathIsAbsolute(string $path): bool
{
return empty($path) ? false : (strspn($path, '/\\', 0, 1) || (mb_strlen($path) > 3 && ctype_alpha($path[0]) && $path[1] === ':' && strspn($path, '/\\', 2, 1)));
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
Condorcet PHP - Election manager and results calculator.
Designed for the Condorcet method. Integrating a large number of algorithms extending Condorcet. Expandable for all types of voting systems.
By Julien Boudry and contributors - MIT LICENSE (Please read LICENSE.txt)
https://github.com/julien-boudry/Condorcet
*/
declare(strict_types=1);
namespace CondorcetPHP\Condorcet\Console\Helper;
use CondorcetPHP\Condorcet\Console\Style\CondorcetStyle;
use CondorcetPHP\Condorcet\{Condorcet, Result};
abstract class FormaterHelper
{
public static function formatResultTable(Result $result): array
{
$resultArray = $result->getResultAsArray(true);
foreach ($resultArray as $rank => &$line) {
if (\is_array($line)) {
$line = implode(',', $line);
}
if ($rank === 1 && \count($result[1]) === 1 && $result[1][0] === $result->getCondorcetWinner()) {
$line .= ' '.CondorcetStyle::CONDORCET_WINNER_SYMBOL_FORMATED;
} elseif ($rank === max(array_keys($resultArray)) && \count($result[max(array_keys($resultArray))]) === 1 && $result[max(array_keys($resultArray))][0] === $result->getCondorcetLoser()) {
$line .= ' '.CondorcetStyle::CONDORCET_LOSER_SYMBOL_FORMATED;
}
$line = [$rank, $line];
}
return $resultArray;
}
public static function prepareMethods(array $methodArgument): array
{
if (empty($methodArgument)) {
return [['name' => Condorcet::getDefaultMethod()::METHOD_NAME[0], 'class' => Condorcet::getDefaultMethod()]];
} else {
$methods = [];
foreach ($methodArgument as $oneMethod) {
if (mb_strtolower($oneMethod) === 'all') {
$methods = Condorcet::getAuthMethods(false);
$methods = array_map(static fn ($m) => ['name' => $m, 'class' => Condorcet::getMethodClass($m)], $methods);
break;
}
if (Condorcet::isAuthMethod($oneMethod)) {
$method_class = Condorcet::getMethodClass($oneMethod);
$method_name = $method_class::METHOD_NAME[0];
if (!\in_array(needle: $method_name, haystack: $methods, strict: true)) {
$methods[] = ['name' => $method_name, 'class' => $method_class];
}
}
}
return $methods;
}
}
}

View File

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

View 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;
}
}

View 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();
}
}
}

View File

@ -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;
}

View File

@ -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 [];
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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';
}

View 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 AlgorithmWithoutRankingFeatureException extends CondorcetPublicApiException
{
protected $message = "This algortihm can't provide a full ranking (but only Winner and Loser)";
}

View 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 CandidateDoesNotExistException extends CondorcetPublicApiException
{
protected $message = 'This candidate does not exist';
}

View 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';
}

View 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 CandidateInvalidNameException extends CondorcetPublicApiException
{
protected $message = 'This name is not valid';
}

View File

@ -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");
}
}

View 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\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);
}
}

View 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';
}

View 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\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) . "'"
);
}
}

View 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 FileDoesNotExistException extends CondorcetPublicApiException
{
protected $message = 'Specified input file does not exist';
}

View File

@ -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;
}

View 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\Internal;
// Custom Exeption
class CondorcetInternalException extends \Exception
{
}

View 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\Internal;
// Custom Exeption
class IntegerOverflowException extends CondorcetInternalException
{
}

View File

@ -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
{
}

View File

@ -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;
}
}
}

View 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';
}

View 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';
}

View 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';
}

View 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 ResultRequestedWithoutVotesException extends CondorcetPublicApiException
{
protected $message = 'The result cannot be requested without votes';
}

View 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 StvQuotaNotImplementedException extends CondorcetPublicApiException
{
protected $message = 'This STV quota is not implemented';
}

View 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';
}

View 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';
}

View 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';
}

View 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 VoteInvalidFormatException extends CondorcetPublicApiException
{
protected $message = 'The format of the vote is invalid';
}

View 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";
}

View 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 VoteMaxNumberReachedException extends CondorcetPublicApiException
{
protected $message = 'The maximal number of votes for the method is reached';
}

View 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';
}

View 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 VotingHasStartedException extends CondorcetPublicApiException
{
protected $message = 'The voting has started';
}

View 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);
}
}

View 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;
}
}

View File

@ -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
};
}
}

View File

@ -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;
}

View 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];
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View 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