|
<?php |
|
|
|
namespace App\Services; |
|
|
|
use Cache; |
|
use Storage; |
|
|
|
class ElectionService { |
|
|
|
/** |
|
* |
|
* @var array |
|
*/ |
|
private $committees = []; |
|
|
|
/** |
|
* |
|
* @var array |
|
*/ |
|
private $regions = []; |
|
|
|
/** |
|
* |
|
* @var integer |
|
*/ |
|
private $digits; |
|
|
|
/** |
|
* |
|
* @var float |
|
*/ |
|
private $step; |
|
|
|
/** |
|
* |
|
* @var string |
|
*/ |
|
private $directory; |
|
|
|
/** |
|
* |
|
* @var array |
|
*/ |
|
private $overallStats = []; |
|
|
|
|
|
/** |
|
* |
|
* @param boolean $prepare prepare committees' data or not |
|
* @param integer $digits decimal digits in percent values |
|
* @param string $directory path relative to /storage/app |
|
*/ |
|
public function __construct($prepare = true, $digits = 2, $directory = 'public/elections') { |
|
$this->digits = $digits; |
|
$this->directory = $directory; |
|
$this->step = 1 / pow(10, $this->digits); |
|
|
|
if($prepare) { |
|
$this->_prepareCommittees(); |
|
} |
|
} |
|
|
|
/** |
|
* prepare clean result for a particular turnout |
|
* @return mixed |
|
*/ |
|
private function _prepareCleanResult() { |
|
return [ |
|
'uik.total' => 0, |
|
'uik.l500' => 0, |
|
'uik.g500l1000' => 0, |
|
'uik.g1000l2000' => 0, |
|
'uik.g2000' => 0, |
|
'voters.total' => 0, |
|
'voters.l500' => 0, |
|
'voters.g500l1000' => 0, |
|
'voters.g1000l2000' => 0, |
|
'voters.g2000' => 0, |
|
]; |
|
} |
|
|
|
/** |
|
* get the key correspondent to committee size |
|
* @param integer $size |
|
* @return string |
|
*/ |
|
public function _getKeyBySize($size) { |
|
return ($size < 500 ? 'l500' : ($size < 1000 ? 'g500l1000' : ($size < 2000 ? 'g1000l2000' : 'g2000'))); |
|
} |
|
|
|
/** |
|
* get committees which are close to a particular percent |
|
* @param NULL|string $region key like tula |
|
* @param float $aroundPercent |
|
* @return array |
|
*/ |
|
private function _getRegionCommittees($region) { |
|
if(!is_null($region)) { |
|
return array_filter($this->committees, function($com) use ($region) { |
|
return ($com['region'] === $region); |
|
}); |
|
} |
|
|
|
return $this->committees; |
|
} |
|
|
|
/** |
|
* building total distribution for committees and voters |
|
* @param NULL|string $region region key like tula |
|
* @return mixed |
|
*/ |
|
private function _buildDistribution($region = NULL) { |
|
$this->_prepareEmptyData(); |
|
|
|
$regionComs = $this->_getRegionCommittees($region); |
|
|
|
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step) |
|
{ |
|
foreach ($regionComs as $committee) { |
|
$this->_checkCommitteeData($ax, $committee); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* prepare grouped array of committees |
|
* @return array |
|
*/ |
|
private function _prepareCommittees() { |
|
$fd = fopen(Storage::path($this->directory . '/edin.csv'), 'r'); |
|
|
|
$k = 0; |
|
|
|
while (($row = fgetcsv($fd, 1000, ",")) !== false) { |
|
# skip first line or empty results (cancelled by CIK etc.) |
|
if (!$k or !isset($row[11])) { |
|
$k++; |
|
continue; |
|
} |
|
|
|
# total voters are valid+invalid+lost |
|
$reg = preg_replace("#^(.*)www\.([^\.]+)\.vybory(.*)$#iUs", "\\2", $row[0]); |
|
$regname = trim($row[2]); |
|
|
|
# add committee and region |
|
$this->_oneCommitteeToGroup($row, $reg); |
|
|
|
if(!isset($this->regions[$reg])) { |
|
$this->regions[$reg] = $regname ?: $reg; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* add committee out of row into particular group |
|
* @param array $row array of a file row |
|
* @param string $reg region key |
|
*/ |
|
private function _oneCommitteeToGroup($row, $reg) { |
|
$this->committees[] = [ |
|
'region' => $reg, |
|
'voters' => $row[9] + $row[10] + $row[11], |
|
'people' => $row[7], |
|
]; |
|
} |
|
|
|
/** |
|
* preparing empty stats arrays |
|
* @return void |
|
*/ |
|
private function _prepareEmptyData() { |
|
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step) |
|
{ |
|
$this->overallStats[$this->_formatTurnout($ax)] = $this->_prepareCleanResult(); |
|
} |
|
} |
|
|
|
/** |
|
* check if the committee fit the floor-or-ceil condition |
|
* @param float $watchingPercent |
|
* @param mixed $committee |
|
*/ |
|
private function _checkCommitteeData($watchingPercent, $committee) { |
|
$key = $this->_getKeyBySize($committee['people']); |
|
|
|
$turnout = $this->_formatTurnout($watchingPercent); |
|
$percent = $watchingPercent * $committee['people'] / 100; |
|
|
|
if ($committee['voters'] == floor($percent) or $committee['voters'] == ceil($percent)) { |
|
$this->overallStats[$turnout]["uik.$key"]++; |
|
$this->overallStats[$turnout]["voters.$key"] += $committee['voters']; |
|
$this->overallStats[$turnout]["uik.total"]++; |
|
$this->overallStats[$turnout]["voters.total"] += $committee['voters']; |
|
} |
|
} |
|
|
|
/** |
|
* store current results into file |
|
* @param string $path |
|
*/ |
|
private function _storeJSON($path) { |
|
Storage::put($path, json_encode([ |
|
'keys' => $this->_prepareCleanResult(), |
|
'data' => $this->overallStats, |
|
])); |
|
|
|
echo(" file $path done at " . date('H:i:s'). ", memory used " . round(memory_get_peak_usage() / 1024 / 1024) ."Mb\n"); |
|
} |
|
|
|
/** |
|
* building total arrays of turnout tries |
|
* @param boolean $rewriteRegions if we need to rewrite file |
|
* @return mixed |
|
*/ |
|
public function buildCountryTurnoutTry($rewriteRegions = false) { |
|
$this->_buildDistribution(); |
|
|
|
$pref = $this->directory . '/2018-regions'; |
|
$this->_storeJSON("$pref-$this->digits-d.json"); |
|
|
|
foreach(array_keys($this->regions) as $region) { |
|
$this->_buildDistribution($region); |
|
|
|
$this->_storeJSON("$pref-$this->digits-d-$region.json"); |
|
} |
|
|
|
if($rewriteRegions) { |
|
Storage::put("$pref.json", json_encode($this->regions, JSON_UNESCAPED_UNICODE)); |
|
} |
|
} |
|
|
|
/** |
|
* get average number |
|
* @param array $sizes |
|
* @return float |
|
*/ |
|
private function _getAverage($sizes) { |
|
return (array_sum($sizes) / count($sizes)); |
|
} |
|
|
|
/** |
|
* get approximate width of a exact-number-peak |
|
* @param array $sizes |
|
* @return float in percents |
|
*/ |
|
private function _getWidthDelta($sizes) { |
|
return 100 / $this->_getAverage($sizes); |
|
} |
|
|
|
/** |
|
* get average size of every group |
|
* @return mixed |
|
*/ |
|
private function _getSizes() { |
|
$sizes = [ |
|
'l500' => [], |
|
'g500l1000' => [], |
|
'g1000l2000' => [], |
|
'g2000' => [], |
|
]; |
|
$totals = []; |
|
|
|
foreach($this->committees as $com) { |
|
$key = $this->_getKeyBySize($com['people']); |
|
|
|
$sizes[$key][] = $com['people']; |
|
$totals[] = $com['people']; |
|
} |
|
|
|
foreach($sizes as $key => $keySizes) { |
|
$sizes[$key] = $this->_getWidthDelta($keySizes); |
|
} |
|
|
|
$sizes['total'] = $this->_getWidthDelta($totals); |
|
|
|
return $sizes; |
|
} |
|
|
|
/** |
|
* get sizes of different size groups |
|
* @return mixed |
|
*/ |
|
public function buildSizes() { |
|
$ret = []; |
|
|
|
foreach($this->_getSizes() as $key => $size) { |
|
$ret[$key] = round($size, 3); |
|
} |
|
|
|
return $ret; |
|
} |
|
|
|
/** |
|
* get widths of peaks by size groups |
|
* @return array |
|
*/ |
|
public function buildWidths() { |
|
$ret = []; |
|
|
|
foreach($this->_getSizes() as $key => $size) { |
|
$ret[$key] = round(100 / $size); |
|
} |
|
|
|
return $ret; |
|
} |
|
|
|
/** |
|
* get region data out of json |
|
* @param string $region region key like tula |
|
* @return mixed |
|
*/ |
|
private function _getRegionData($region) { |
|
$prefix = "$this->directory/2018-regions-$this->digits"; |
|
|
|
return json_decode(Storage::get("$prefix-d-$region.json"), true); |
|
} |
|
|
|
/** |
|
* get results for selection regions only |
|
* @param array $regions |
|
* @return mixed |
|
*/ |
|
private function _buildRegionsGrouped($regions) { |
|
$this->_prepareEmptyData(); |
|
|
|
foreach($regions as $region) { |
|
$data = $this->_getRegionData($region); |
|
|
|
for ($ax = 0; $ax < (100 + $this->step); $ax = $ax + $this->step) { |
|
$this->_addToOverallStats($this->_formatTurnout($ax), $data['data']); |
|
} |
|
} |
|
|
|
return ['keys' => $this->_prepareCleanResult(), 'data' => $this->overallStats]; |
|
} |
|
|
|
private function _addToOverallStats($turnout, $data) { |
|
foreach($data[$turnout] as $keyname => $value) { |
|
$this->overallStats[$turnout][$keyname] += $value; |
|
} |
|
} |
|
|
|
/** |
|
* get results for selection regions only |
|
* @param array $regions |
|
* @return mixed |
|
*/ |
|
public function getRegionsGrouped($regions) { |
|
return Cache::remember('elections-group-' . join('-', $regions) . '-' . $this->digits, 1440, function() use ($regions){ |
|
return $this->_buildRegionsGrouped($regions); |
|
}); |
|
} |
|
|
|
/** |
|
* formatting percent to exact decimal digits |
|
* @param type $percent |
|
* @return type |
|
*/ |
|
private function _formatTurnout($percent) { |
|
return number_format($percent, $this->digits, ".", ""); |
|
} |
|
} |