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