Merge pull request #2335 from int2001/label_fix_ascii

Label fix ascii
This commit is contained in:
Peter Goodhall
2023-07-28 11:23:50 +01:00
committed by GitHub
52 changed files with 8506 additions and 110 deletions

View File

@@ -1,5 +1,6 @@
<?php
require_once './src/Label/vendor/autoload.php';
use Cloudlog\Label\PDF_Label;
use Cloudlog\Label\tfpdf;
use Cloudlog\Label\font\unifont\ttfonts;
@@ -7,7 +8,7 @@ use Cloudlog\Label\font\unifont\ttfonts;
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Labels extends CI_Controller {
/*
/*
|--------------------------------------------------------------------------
| Controller: Labels
|--------------------------------------------------------------------------
@@ -15,7 +16,7 @@ class Labels extends CI_Controller {
| This Controller handles all things Labels, creating, editing and printing
|
|
*/
*/
function __construct() {
parent::__construct();
@@ -34,7 +35,7 @@ class Labels extends CI_Controller {
| Nothing fancy just shows the main display of how many labels are waiting
| to be printed per station profile.
|
*/
*/
public function index() {
$data['page_title'] = "QSL Card Labels";
@@ -52,7 +53,7 @@ class Labels extends CI_Controller {
$this->load->view('interface_assets/header', $data);
$this->load->view('labels/index');
$this->load->view('interface_assets/footer', $footerData);
}
/*
@@ -62,9 +63,9 @@ class Labels extends CI_Controller {
|
| Shows the form used to create a label type.
|
*/
*/
public function create() {
$data['page_title'] = "Create Label Type";
$this->load->library('form_validation');
@@ -81,10 +82,10 @@ class Labels extends CI_Controller {
{
$this->load->model('labels_model');
$this->labels_model->addLabel();
redirect('labels');
}
}
public function printids() {
@@ -103,11 +104,13 @@ class Labels extends CI_Controller {
$this->prepareLabel($result);
}
function prepareLabel($qsos, $jscall = false) {
$this->load->model('labels_model');
$label = $this->labels_model->getDefaultLabel();
$label->font='DejaVuSans'; // Fix font to DejaVuSans
try {
if ($label) {
$pdf = new PDF_Label(array(
@@ -144,20 +147,20 @@ class Labels extends CI_Controller {
}
}
define('FPDF_FONTPATH', './src/Label/font/');
$pdf->AddPage();
if ($label->font == 'DejaVuSans') {
if ($label->font == 'DejaVuSans') { // leave this here, for future Use
$pdf->AddFont($label->font,'','DejaVuSansMono.ttf',true);
$pdf->SetFont($label->font);
} else {
$pdf->AddFont($label->font);
$pdf->SetFont($label->font);
}
if ($qsos->num_rows() > 0) {
if ($label->qsos == 1) {
$this->makeOneQsoLabel($qsos->result(), $pdf);
$this->makeMultiQsoLabel($qsos->result(), $pdf,1);
} else {
$this->makeMultiQsoLabel($qsos->result(), $pdf, $label->qsos);
}
@@ -168,31 +171,19 @@ class Labels extends CI_Controller {
$pdf->Output();
}
function makeOneQsoLabel($qsos, $pdf) {
foreach($qsos as $qso) {
$time = strtotime($qso->COL_TIME_ON);
$myFormatForView = date("d/m/Y H:i", $time);
if($qso->COL_SAT_NAME != "") {
$text = sprintf("%s\n\n%s %s\n%s %s \n\n%s", 'To: '.$qso->COL_CALL, $myFormatForView, 'on '.$qso->COL_BAND.' 2x'.$qso->COL_MODE.' RST '.$qso->COL_RST_SENT.'', 'Satellite: '.$qso->COL_SAT_NAME.' Mode: '.strtoupper($qso->COL_SAT_MODE).' ', '', 'Thanks for QSO.');
} else {
$text = sprintf("%s\n\n%s %s\n%s %s \n\n%s", 'To: '.$qso->COL_CALL, $myFormatForView, 'on '.$qso->COL_BAND.' 2x'.$qso->COL_MODE.' RST '.$qso->COL_RST_SENT.'', '', '', 'Thanks for QSO.');
}
$pdf->Add_Label($text);
}
}
function makeMultiQsoLabel($qsos, $pdf, $numberofqsos) {
$text = '';
$current_callsign = '';
$current_sat = '';
$qso_data = [];
foreach($qsos as $qso) {
if ($qso->COL_CALL !== $current_callsign) {
if (($qso->COL_SAT_NAME !== $current_sat) || ($qso->COL_CALL !== $current_callsign)) {
if (!empty($qso_data)) {
$this->makeLabel($pdf, $current_callsign, $qso_data, $numberofqsos);
$this->finalizeData($pdf, $current_callsign, $qso_data, $numberofqsos);
$qso_data = [];
}
$current_callsign = $qso->COL_CALL;
$current_sat = $qso->COL_SAT_NAME;
}
$qso_data[] = [
@@ -203,47 +194,63 @@ class Labels extends CI_Controller {
'mygrid' => $qso->station_gridsquare,
'sat' => $qso->COL_SAT_NAME,
'sat_mode' => $qso->COL_SAT_MODE,
'qsl_recvd' => $qso->COL_QSL_RCVD
];
}
if (!empty($qso_data)) {
$this->makeLabel($pdf, $current_callsign, $qso_data, $numberofqsos);
$this->finalizeData($pdf, $current_callsign, $qso_data, $numberofqsos);
}
}
// New begin
function makeLabel($pdf, $current_callsign, $qso_data, $numberofqsos) {
$text = 'To: ' . $current_callsign . "\n\n";
$count = 0;
$qsotext = '';
foreach ($qso_data as $key => $qso) {
function finalizeData($pdf, $current_callsign, &$preliminaryData, $qso_per_label) {
$tableData = [];
$count_qso = 0;
$qso=[];
foreach ($preliminaryData as $key => $row) {
$qso=$row;
$time = strtotime($qso['time']);
$myFormatForView = date("d/m/Y H:i", $time);
$myFormatForView = date("Y-m-d H:i", $time);
$rowData = [
'Date/Time (UTC)' => $myFormatForView,
'Band' => $row['band'],
'Mode' => $row['mode'],
'RST' => $row['rst'],
];
$tableData[] = $rowData;
$count_qso++;
if($qso['sat'] != "") {
$qsotext .= sprintf("%s %s %s %s\n", $myFormatForView, 'on '.$qso['band'].' 2x'.$qso['mode'].' RST '.$qso['rst'].'', 'Satellite: '.$qso['sat'].' Mode: '.strtoupper($qso['sat_mode']).' ', '');
} else {
$qsotext .= sprintf("%s %s\n", $myFormatForView, 'on '.$qso['band'].' 2x'.$qso['mode'].' RST '.$qso['rst']);
if($count_qso == $qso_per_label){
$this->generateLabel($pdf, $current_callsign, $tableData,$count_qso,$qso);
$tableData = []; // reset the data
$count_qso = 0; // reset the counter
}
$count++;
if ($count == $numberofqsos) {
$text .= $qsotext;
$text .= "\n" . 'Thanks for QSOs.';
$pdf->Add_Label($text);
$text = 'To: ' . $current_callsign . "\n\n";
$count = 0;
$qsotext = '';
}
unset($qso_data[$key]);
unset($preliminaryData[$key]);
}
if ($qsotext != '') {
$text .= $qsotext;
$text .= "\n" . 'Thanks for QSOs.';
$pdf->Add_Label($text);
// generate label for remaining QSOs
if($count_qso > 0){
$this->generateLabel($pdf, $current_callsign, $tableData,$count_qso,$qso);
$preliminaryData = []; // reset the data
}
}
function generateLabel($pdf, $current_callsign, $tableData,$numofqsos,$qso){
$builder = new \AsciiTable\Builder();
$builder->addRows($tableData);
$text = "Confirming QSO".($numofqsos>1 ? 's' : '')." with ";
$text .= $current_callsign;
$text .= "\n";
$text .= $builder->renderTable();
if($qso['sat'] != "") {
$text .= "\n".'Satellite: '.$qso['sat'].' Mode: '.strtoupper($qso['sat_mode'][0]).'/'.strtoupper($qso['sat_mode'][1]);
}
$text .= "\nThanks for the QSO".($numofqsos>1 ? 's' : '');
$text .= " | ".($qso['qsl_recvd'] == 'Y' ? 'TNX' : 'PSE')." QSL";
$pdf->Add_Label($text);
}
// New End
public function edit($id) {
$this->load->model('labels_model');
@@ -278,4 +285,4 @@ class Labels extends CI_Controller {
$this->labels_model->saveDefaultLabel($id);
}
}
}

View File

@@ -123,6 +123,10 @@ class Labels_model extends CI_Model {
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->where_in('COL_QSL_SENT', array('R', 'Q'));
$this->db->order_by("COL_DXCC", "ASC");
$this->db->order_by("COL_CALL", "ASC");
$this->db->order_by("COL_SAT_NAME", "ASC");
$this->db->order_by("COL_TIME_ON", "ASC");
$this->db->order_by("COL_MODE", "ASC");
$query = $this->db->get($this->config->item('table_name'));
return $query;
@@ -139,4 +143,4 @@ class Labels_model extends CI_Model {
return $query;
}
}
}

View File

@@ -111,29 +111,6 @@
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="font">Font</label>
<div class="col-sm-4">
<select name="font" class="form-control" id="font">
<option value="courier">Courier</option>
<option value="courierb">Courierb</option>
<option value="courierbi">Courierbi</option>
<option value="courieri">Courieri</option>
<option value="DejaVuSans">DejaVuSans</option>
<option value="helvetica">Helvetica</option>
<option value="helveticab">Helveticab</option>
<option value="helveticabi">Helveticabi</option>
<option value="helveticai">Helveticai</option>
<option value="symbol">Symbol</option>
<option value="times">Times</option>
<option value="timesb">Timesb</option>
<option value="timesb">Timesb</option>
<option value="timesbi">Timesbi</option>
<option value="zapfdingbats">Zapfdingbats</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-square"></i> Save Label Type</button>
</div>
</div>
@@ -141,4 +118,4 @@
</form>
</div>
<br>
<br>

View File

@@ -110,30 +110,6 @@
<input name="label_qsos" type="number" min="1" max="40" step="1" class="form-control" id="label_qsos" aria-describedby="font_sizeHelp" value="<?php if(isset($label->qsos)) { echo $label->qsos; } ?>">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="font">Font</label>
<div class="col-sm-4">
<select name="font" class="form-control" id="font">
<option value="courier" <?php if($label->font == "courier") { echo "selected=\"selected\""; } ?>>Courier</option>
<option value="courierb" <?php if($label->font == "courierb") { echo "selected=\"selected\""; } ?>>Courierb</option>
<option value="courierbi" <?php if($label->font == "courierbi") { echo "selected=\"selected\""; } ?>>Courierbi</option>
<option value="courieri" <?php if($label->font == "courieri") { echo "selected=\"selected\""; } ?>>Courieri</option>
<option value="DejaVuSans" <?php if($label->font == "DejaVuSans") { echo "selected=\"selected\""; } ?>>DejaVuSans</option>
<option value="helvetica" <?php if($label->font == "helvetica") { echo "selected=\"selected\""; } ?>>Helvetica</option>
<option value="helveticab" <?php if($label->font == "helveticab") { echo "selected=\"selected\""; } ?>>Helveticab</option>
<option value="helveticabi" <?php if($label->font == "helveticabi") { echo "selected=\"selected\""; } ?>>Helveticabi</option>
<option value="helveticai" <?php if($label->font == "helveticai") { echo "selected=\"selected\""; } ?>>Helveticai</option>
<option value="symbol" <?php if($label->font == "symbol") { echo "selected=\"selected\""; } ?>>Symbol</option>
<option value="times" <?php if($label->font == "times") { echo "selected=\"selected\""; } ?>>Times</option>
<option value="timesb" <?php if($label->font == "timesb") { echo "selected=\"selected\""; } ?>>Timesb</option>
<option value="timesb" <?php if($label->font == "timesb") { echo "selected=\"selected\""; } ?>>Timesb</option>
<option value="timesbi" <?php if($label->font == "timesbi") { echo "selected=\"selected\""; } ?>>Timesbi</option>
<option value="zapfdingbats" <?php if($label->font == "zapfdingbats") { echo "selected=\"selected\""; } ?>>Zapfdingbats</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-square"></i> Save Label Type</button>
</div>
</div>
@@ -141,4 +117,4 @@
</form>
</div>
<br>
<br>

25
src/Label/vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit6ef2b7bf933fafec62d6bf7c15d6f12f::getLoader();

View File

@@ -0,0 +1,585 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
src/Label/vendor/composer/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'dekor\\' => array($vendorDir . '/dekor/php-array-table/src'),
'Ds\\' => array($vendorDir . '/php-ds/php-ds/src'),
'AsciiTable\\' => array($vendorDir . '/malios/php-to-ascii-table/src'),
);

View File

@@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit6ef2b7bf933fafec62d6bf7c15d6f12f
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit6ef2b7bf933fafec62d6bf7c15d6f12f', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit6ef2b7bf933fafec62d6bf7c15d6f12f', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit6ef2b7bf933fafec62d6bf7c15d6f12f::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,44 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit6ef2b7bf933fafec62d6bf7c15d6f12f
{
public static $prefixLengthsPsr4 = array (
'D' =>
array (
'Ds\\' => 3,
),
'A' =>
array (
'AsciiTable\\' => 11,
),
);
public static $prefixDirsPsr4 = array (
'Ds\\' =>
array (
0 => __DIR__ . '/..' . '/php-ds/php-ds/src',
),
'AsciiTable\\' =>
array (
0 => __DIR__ . '/..' . '/malios/php-to-ascii-table/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit6ef2b7bf933fafec62d6bf7c15d6f12f::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit6ef2b7bf933fafec62d6bf7c15d6f12f::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit6ef2b7bf933fafec62d6bf7c15d6f12f::$classMap;
}, null, ClassLoader::class);
}
}

171
src/Label/vendor/composer/installed.json vendored Normal file
View File

@@ -0,0 +1,171 @@
{
"packages": [
{
"name": "dekor/php-array-table",
"version": "2.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/deniskoronets/php-array-table.git",
"reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/deniskoronets/php-array-table/zipball/ca40b21ba84eee6a9658a33fc5f897d76baaf8e5",
"reference": "ca40b21ba84eee6a9658a33fc5f897d76baaf8e5",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.6.0"
},
"require-dev": {
"phpunit/phpunit": "^10"
},
"time": "2023-02-10T10:13:42+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"dekor\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Denis Koronets",
"email": "deniskoronets@woo.zp.ua",
"homepage": "https://woo.zp.ua/"
}
],
"description": "PHP Library for printing associative arrays as text table (similar to mysql terminal console)",
"keywords": [
"library",
"php"
],
"support": {
"issues": "https://github.com/deniskoronets/php-array-table/issues",
"source": "https://github.com/deniskoronets/php-array-table/tree/2.0"
},
"install-path": "../dekor/php-array-table"
},
{
"name": "malios/php-to-ascii-table",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/malios/php-to-ascii-table.git",
"reference": "1a4621f5286f72ff0823627088e94382546b9218"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/malios/php-to-ascii-table/zipball/1a4621f5286f72ff0823627088e94382546b9218",
"reference": "1a4621f5286f72ff0823627088e94382546b9218",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7|^8",
"php-ds/php-ds": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "@stable",
"vimeo/psalm": "@stable"
},
"time": "2022-02-13T20:15:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"AsciiTable\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mehmed Aliosman",
"email": "dev.mehmed.aliosman@gmail.com"
}
],
"description": "A PHP library to generate plain text tables.",
"keywords": [
"ascii",
"php",
"plain text table",
"table"
],
"support": {
"issues": "https://github.com/malios/php-to-ascii-table/issues",
"source": "https://github.com/malios/php-to-ascii-table/tree/v3.0.0"
},
"install-path": "../malios/php-to-ascii-table"
},
{
"name": "php-ds/php-ds",
"version": "v1.4.1",
"version_normalized": "1.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-ds/polyfill.git",
"reference": "43d2df301a9e2017f67b8c11d94a5222f9c00fd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-ds/polyfill/zipball/43d2df301a9e2017f67b8c11d94a5222f9c00fd1",
"reference": "43d2df301a9e2017f67b8c11d94a5222f9c00fd1",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.0"
},
"provide": {
"ext-ds": "1.3.0"
},
"require-dev": {
"php-ds/tests": "^1.3"
},
"suggest": {
"ext-ds": "to improve performance and reduce memory usage"
},
"time": "2022-03-09T20:39:30+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Ds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rudi Theunissen",
"email": "rudolf.theunissen@gmail.com"
}
],
"keywords": [
"data structures",
"ds",
"php",
"polyfill"
],
"support": {
"issues": "https://github.com/php-ds/polyfill/issues",
"source": "https://github.com/php-ds/polyfill/tree/v1.4.1"
},
"install-path": "../php-ds/php-ds"
}
],
"dev": true,
"dev-package-names": []
}

50
src/Label/vendor/composer/installed.php vendored Normal file
View File

@@ -0,0 +1,50 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '377e3875ddde0f2437686215a845f75230f7dd17',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '377e3875ddde0f2437686215a845f75230f7dd17',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'dekor/php-array-table' => array(
'pretty_version' => '2.0',
'version' => '2.0.0.0',
'reference' => 'ca40b21ba84eee6a9658a33fc5f897d76baaf8e5',
'type' => 'library',
'install_path' => __DIR__ . '/../dekor/php-array-table',
'aliases' => array(),
'dev_requirement' => false,
),
'malios/php-to-ascii-table' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => '1a4621f5286f72ff0823627088e94382546b9218',
'type' => 'library',
'install_path' => __DIR__ . '/../malios/php-to-ascii-table',
'aliases' => array(),
'dev_requirement' => false,
),
'php-ds/php-ds' => array(
'pretty_version' => 'v1.4.1',
'version' => '1.4.1.0',
'reference' => '43d2df301a9e2017f67b8c11d94a5222f9c00fd1',
'type' => 'library',
'install_path' => __DIR__ . '/../php-ds/php-ds',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70000)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.0.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,2 @@
.idea/
vendor/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Mehmed Aliosman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,116 @@
# php-to-ascii-table
A small PHP library for generating plain text tables.
![example table](assets/table.png)
## Getting Started
### Prerequisites
- PHP >= 7
- ext-mbstring
- Optionally Install [php-ds](https://github.com/php-ds/extension) extension (Recommended).
### Installation
Install via composer:
```bash
$ composer require malios/php-to-ascii-table
```
## Usage
```php
<?php
$builder = new \AsciiTable\Builder();
$builder->addRows([
[
'Order No' => 'A0001',
'Product Name' => 'Intel CPU',
'Price' => 700.00,
'Quantity' => 1
],
[
'Order No' => 'A0002',
'Product Name' => 'Hard disk 10TB',
'Price' => 500.00,
'Quantity' => 2
],
[
'Order No' => 'A0003',
'Product Name' => 'Dell Laptop',
'Price' => 11600.00,
'Quantity' => 8
],
[
'Order No' => 'A0004',
'Product Name' => 'Intel CPU',
'Price' => 5200.00,
'Quantity' => 3
]
]);
$builder->addRow([
'Order No' => 'A0005',
'Product Name' => 'A4Tech Mouse',
'Price' => 100.00,
'Quantity' => 10
]);
$builder->setTitle('Product List');
echo $builder->renderTable();
// Show only some fields
$builder->showColumns(['Order No', 'Product Name', 'Quantity']);
echo $builder->renderTable();
```
### Build table from objects
You can build table form any object that implements JsonSerializable interface.
```php
<?php
class Person implements \JsonSerializable
{
private $name;
private $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
public function jsonSerialize()
{
return [
'name' => $this->name,
'age' => $this->age
];
}
}
$builder = new \AsciiTable\Builder();
$builder->addRow(new Person('John', 25));
$builder->addRow(new Person('Bill', 30));
echo $builder->renderTable();
```
## Contributing
All contributors are welcome. You can open a new issue or submit a pull request.
See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for details.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -0,0 +1,46 @@
{
"name": "malios/php-to-ascii-table",
"description": "A PHP library to generate plain text tables.",
"type": "library",
"keywords": [
"php",
"table",
"ascii",
"plain text table"
],
"authors": [
{
"name": "Mehmed Aliosman",
"email": "dev.mehmed.aliosman@gmail.com"
}
],
"license": "MIT",
"require": {
"php": "^7|^8",
"php-ds/php-ds": "^1.1",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "@stable",
"vimeo/psalm": "@stable"
},
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"AsciiTable\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"AsciiTable\\Test\\": "tests/"
}
},
"scripts": {
"test": [
"@phpunit",
"@psalm"
],
"phpunit": "phpunit tests/ colors=always",
"psalm": "psalm"
}
}

2585
src/Label/vendor/malios/php-to-ascii-table/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
## Contributing
1. Fork project
2. Create your feature branch: `git checkout -b my-new-feature`
3. Make changes
4. Run tests: `php vendor/bin/phpunit tests/`
5. Commit your changes: `git commit -am 'Add some feature'`
6. Push to the branch: `git push origin my-new-feature`
7. Submit a pull request

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<DeprecatedProperty errorLevel="info" />
<DeprecatedClass errorLevel="info" />
<DeprecatedConstant errorLevel="info" />
<DeprecatedFunction errorLevel="info" />
<DeprecatedInterface errorLevel="info" />
<DeprecatedTrait errorLevel="info" />
<InternalMethod errorLevel="info" />
<InternalProperty errorLevel="info" />
<InternalClass errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<MisplacedRequiredParam errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<MissingClosureParamType errorLevel="info" />
<MissingParamType errorLevel="info" />
<RedundantCondition errorLevel="info" />
<DocblockTypeContradiction errorLevel="info" />
<RedundantConditionGivenDocblockType errorLevel="info" />
<UnresolvableInclude errorLevel="info" />
<RawObjectIteration errorLevel="info" />
<InvalidStringClass errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,253 @@
<?php declare(strict_types=1);
namespace AsciiTable;
use AsciiTable\Exception\BuilderException;
use Ds\Collection;
class Builder
{
/**
* @var string
*/
const CHAR_CELL_SEPARATOR = '│';
/**
* @var string
*/
const CHAR_LINE_SEPARATOR = '─';
/**
* @var string
*/
const CHAR_CELL_PADDING = ' ';
/**
* @var string
*/
const CHAR_JOIN_INNER = '┼';
/**
* @var string
*/
const CHAR_CORNER_TOP_LEFT = '┌';
/**
* @var string
*/
const CHAR_CORNER_TOP_RIGHT = '┐';
/**
* @var string
*/
const CHAR_JOIN_LEFT_INNER = '├';
/**
* @var string
*/
const CHAR_JOIN_RIGHT_INNER = '┤';
/**
* @var string
*/
const CHAR_JOIN_TOP_INNER = '┬';
/**
* @var string
*/
const CHAR_JOIN_BOTTOM_INNER = '┴';
/**
* @var string
*/
const CHAR_CORNER_BOTTOM_LEFT = '└';
/**
* @var string
*/
const CHAR_CORNER_BOTTOM_RIGHT = '┘';
/**
* @var Table
*/
private $table;
/**
* @var string|null
*/
private $title;
public function __construct()
{
$this->table = new Table();
}
/**
* Get the table
*
* @return Table
*/
public function getTable() : Table
{
return $this->table;
}
/**
* Add single row.
* The value passed should be either an array or an JsonSerializable object
*
* @param array|\JsonSerializable $rowArrayOrObject
* @throws BuilderException
*/
public function addRow($rowArrayOrObject)
{
if (is_array($rowArrayOrObject)) {
$rowArray = $rowArrayOrObject;
} else if ($rowArrayOrObject instanceof \JsonSerializable) {
$rowArray = $rowArrayOrObject->jsonSerialize();
} else {
throw new BuilderException(sprintf(
'Row must be either an array or JsonSerializable, %s given instead',
gettype($rowArrayOrObject)
));
}
$row = new Row();
foreach ($rowArray as $columnName => $value) {
$cell = new Cell($columnName, $value);
$row->addCell($cell);
}
$this->table->addRow($row);
}
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* Add multiple rows
*
* @param array[]|\JsonSerializable[] $rows
* @return void
*/
public function addRows(array $rows)
{
foreach ($rows as $row) {
$this->addRow($row);
}
}
/**
* Show only specific columns of the table
*
* @param array $columnNames
* @return void
* @throws BuilderException
*/
public function showColumns(array $columnNames)
{
$this->table->setVisibleColumns($columnNames);
}
/**
* Render table and return result string
*
* @return string
* @throws BuilderException
*/
public function renderTable() : string
{
if ($this->table->isEmpty()) throw new BuilderException('Cannot render empty table');
$visibleColumns = $this->table->getVisibleColumns();
// border for header and footer
$borderParts = array_map(function ($columnName) {
$width = $this->table->getColumnWidth($columnName);
return str_repeat(self::CHAR_LINE_SEPARATOR, ($width + 2));
}, $visibleColumns->toArray());
$borderTop = self::CHAR_CORNER_TOP_LEFT
. join(self::CHAR_JOIN_TOP_INNER, $borderParts)
. self::CHAR_CORNER_TOP_RIGHT;
$borderMiddle = self::CHAR_JOIN_LEFT_INNER
. join(self::CHAR_JOIN_INNER, $borderParts)
. self::CHAR_JOIN_RIGHT_INNER;
$borderBottom = self::CHAR_CORNER_BOTTOM_LEFT
. join(self::CHAR_JOIN_BOTTOM_INNER, $borderParts)
. self::CHAR_CORNER_BOTTOM_RIGHT;
$headerCells = array_map(function ($columnName) {
return new Cell($columnName, $columnName);
}, $visibleColumns->toArray());
$headerRow = new Row();
$headerRow->addCells(...$headerCells);
$header = $this->renderRow($headerRow, $visibleColumns);
$body = '';
$rows = $this->table->getRows();
$visibleColumns = $this->table->getVisibleColumns();
foreach ($rows as $row) {
$currentLine = $this->renderRow($row, $visibleColumns);
$body .= $currentLine . PHP_EOL;
}
if ($this->title === null) {
$titleString = '';
} else {
$titlePadding = intdiv(max(0, mb_strwidth($borderTop) - mb_strwidth($this->title)), 2);
$titleString = str_repeat(' ', $titlePadding) . $this->title . PHP_EOL;
}
$tableAsString = $titleString . $borderTop . PHP_EOL . $header . PHP_EOL . $borderMiddle . PHP_EOL . $body . $borderBottom;
return $tableAsString;
}
/**
* Render single row and return string
*
* @param RowInterface $row
* @param Collection $columnNames
* @return string
*/
private function renderRow(RowInterface $row, Collection $columnNames)
{
$line = self::CHAR_CELL_SEPARATOR;
// render cells of the row
foreach ($columnNames as $columnName) {
$colWidth = $this->table->getColumnWidth($columnName);
if ($row->hasCell($columnName)) {
$cell = $row->getCell($columnName);
$currentCell = $this->renderCell($cell, $colWidth);
} else {
$currentCell = $this->renderCell(new Cell($columnName, ''), $colWidth);
}
$line .= $currentCell . self::CHAR_CELL_SEPARATOR;
}
return $line;
}
/**
* Render cell content with left and right padding depending on the column width
*
* @param CellInterface $cell
* @param int $colWidth
* @return string
*/
private function renderCell(CellInterface $cell, int $colWidth) : string
{
$filler = str_repeat(self::CHAR_CELL_PADDING, ($colWidth - $cell->getWidth()));
if ($cell->getAlign() == Cell::ALIGN_LEFT) {
$content = self::CHAR_CELL_PADDING . $cell->getValue() . $filler . self::CHAR_CELL_PADDING;
} else {
$content = self::CHAR_CELL_PADDING . $filler . $cell->getValue() . self::CHAR_CELL_PADDING;
}
return $content;
}
}

View File

@@ -0,0 +1,105 @@
<?php declare(strict_types=1);
namespace AsciiTable;
class Cell implements CellInterface
{
/**
* int
*/
const ALIGN_LEFT = 0;
/**
* int
*/
const ALIGN_RIGHT = 1;
/**
* The name of the column that the cell belongs to
*
* @var string
*/
private $columnName;
/**
* @var string
*/
private $value;
/**
* @var int
*/
private $align = self::ALIGN_LEFT;
/**
* @var int
*/
private $width = 0;
public function __construct($columnName, $value = '')
{
$this->setColumnName($columnName);
$this->setValue($value);
}
/**
* {@inheritdoc}
*/
public function getValue() : string
{
return $this->value;
}
/**
* {@inheritdoc}
*/
public function setValue($value)
{
if (is_float($value)) {
$round = round($value);
if (($value - $round) === (float)0) {
$this->value = number_format($value, 2, '.', ' ');
} else {
$this->value = (string) $value;
}
$this->align = self::ALIGN_RIGHT;
} elseif (is_int($value)) {
$this->value = (string) $value;
$this->align = self::ALIGN_RIGHT;
} else {
$this->value = (string) $value;
$this->align = self::ALIGN_LEFT;
}
$this->width = mb_strwidth($this->value);
}
public function getAlign(): int
{
return $this->align;
}
/**
* {@inheritdoc}
*/
public function getColumnName() : string
{
return $this->columnName;
}
/**
* {@inheritdoc}
*/
public function setColumnName(string $columnName)
{
$this->columnName = $columnName;
}
/**
* {@inheritdoc}
*/
public function getWidth() : int
{
return $this->width;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace AsciiTable;
interface CellInterface
{
/**
* Get value of the cell
*
* @return string
*/
public function getValue() : string;
/**
* Set value of the cell.
*
* @param mixed $value
*/
public function setValue($value);
/**
* Get the name of the column that the cell belongs to
*
* @return string
*/
public function getColumnName() : string;
/**
* Get the alignment of the cell
*
* @return int
*/
public function getAlign(): int;
/**
* Set the name of the column that the cell belongs to
*
* @param string $columnName
*/
public function setColumnName(string $columnName);
/**
* Get the width (string length) of the cell
*
* @return int
*/
public function getWidth() : int;
}

View File

@@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace AsciiTable\Exception;
use Exception;
class BuilderException extends Exception{ }

View File

@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace AsciiTable;
use Ds\Map;
use Ds\Collection;
class Row implements RowInterface
{
/**
* @var Map
*/
private $cells;
public function __construct()
{
$this->cells = new Map();
}
/**
* {@inheritdoc}
*/
public function addCell(CellInterface $cell)
{
$this->cells->put($cell->getColumnName(), $cell);
}
/**
* {@inheritdoc}
*/
public function addCells(CellInterface ...$cells)
{
foreach ($cells as $cell) {
$this->addCell($cell);
}
}
/**
* {@inheritdoc}
*/
public function getCell($columnName) : CellInterface
{
return $this->cells->get($columnName);
}
/**
* {@inheritdoc}
*/
public function hasCell($columnName) : bool
{
return $this->cells->hasKey($columnName);
}
/**
* {@inheritdoc}
*/
public function getCells() : Collection
{
return $this->cells;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace AsciiTable;
use Ds\Collection;
interface RowInterface
{
/**
* Add single cell to the row
*
* @param CellInterface $cell
*/
public function addCell(CellInterface $cell);
/**
* Add multiple cells to row
*
* @param CellInterface ...$cells
*/
public function addCells(CellInterface ...$cells);
/**
* Get single cell by name
*
* @param $columnName
* @return CellInterface
*/
public function getCell($columnName) : CellInterface;
/**
* Check if the row has a cell cell for given column
*
* @param $columnName
* @return bool
*/
public function hasCell($columnName) : bool;
/**
* Get all cells
*
* @return Collection
*/
public function getCells() : Collection;
}

View File

@@ -0,0 +1,123 @@
<?php declare(strict_types=1);
namespace AsciiTable;
use Ds\Map;
use Ds\Set;
class Table implements TableInterface
{
/**
* @var RowInterface[]
*/
private $rows = [];
/**
* @var Set
*/
private $visibleColumns;
/**
* @var Set
*/
private $allColumns;
/**
* @var Map
*/
private $biggestValues;
public function __construct()
{
$this->visibleColumns = new Set();
$this->allColumns = new Set();
$this->biggestValues = new Map();
}
/**
* {@inheritdoc}
*/
public function addRow(RowInterface $row)
{
foreach ($row->getCells() as $cell) {
$columnName = $cell->getColumnName();
$this->allColumns->add($columnName);
$width = $cell->getWidth();
if ($this->biggestValues->hasKey($columnName)) {
if ($width > $this->biggestValues->get($columnName)) {
$this->biggestValues->put($columnName, $width);
}
} else {
$this->biggestValues->put($columnName, $width);
}
}
array_push($this->rows, $row);
}
/**
* {@inheritdoc}
*/
public function getRows() : array
{
return $this->rows;
}
/**
* {@inheritdoc}
*/
public function isEmpty() : bool
{
return empty($this->rows);
}
/**
* {@inheritdoc}
*/
public function setVisibleColumns(array $columnNames)
{
$this->visibleColumns->clear();
$this->visibleColumns->allocate(count($columnNames));
$this->visibleColumns->add(...$columnNames);
}
/**
* {@inheritdoc}
*/
public function getVisibleColumns() : Set
{
if ($this->visibleColumns->isEmpty()) {
return $this->getAllColumns();
}
return $this->visibleColumns;
}
/**
* {@inheritdoc}
*/
public function getAllColumns() : Set
{
return $this->allColumns;
}
/**
* {@inheritdoc}
*/
public function getColumnWidth(string $columnName) : int
{
$width = 0;
if ($this->biggestValues->hasKey($columnName)) {
$width = $this->biggestValues->get($columnName);
}
$visibleColumns = $this->getVisibleColumns();
if ($visibleColumns->contains($columnName) && mb_strwidth($columnName) > $width) {
$width = mb_strwidth($columnName);
}
return $width;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace AsciiTable;
use Ds\Set;
interface TableInterface
{
/**
* Add single row to the table
*
* @param RowInterface $row
*/
public function addRow(RowInterface $row);
/**
* Get all rows in the table
*
* @return RowInterface[]
*/
public function getRows() : array;
/**
* Check if the table is empty.
*
* @return bool
*/
public function isEmpty() : bool;
/**
* Set visible columns
*
* @param string[] $columnNames
*/
public function setVisibleColumns(array $columnNames);
/**
* Get visible columns
*
* @return Set
*/
public function getVisibleColumns() : Set;
/**
* Get all columns in the table
*
* @return Set
*/
public function getAllColumns() : Set;
/**
* Get the width of a column by name
*
* @param string $columnName
* @return int
*/
public function getColumnWidth(string $columnName) : int;
}

View File

@@ -0,0 +1,33 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [1.3.0] - 2020-10-13
### Changed
- Implement ArrayAccess consistently
### Fixed
- Return types were incorrectly nullable in some cases
- Deque capacity was inconsistent with the extension
## [1.2.0] - 2017-08-03
### Changed
- Minor capacity updates
## [1.1.1] - 2016-08-09
### Fixed
- `Stack` and `Queue` array access should throw `OutOfBoundsException`, not `Error`.
### Improved
- Added a lot of docblock comments that were missing.
## [1.1.0] - 2016-08-04
### Added
- `Pair::copy`
## [1.0.3] - 2016-08-01
### Added
- `Set::merge`
## [1.0.2] - 2016-07-31
### Added
- `Map::putAll`

View File

@@ -0,0 +1,21 @@
# Contributing
Contributions are accepted via [pull requests](https://github.com/php-ds/ext/pulls). If you would like to report a bug, please create an [issue](https://github.com/php-ds/ext/issues) instead.
## Issues
- **How to reproduce** - Provide an easy way to reproduce the bug. This makes it easier for others to debug.
- **Platform details** - Specify your platform and your PHP version, eg. "PHP 7.0.2 on Ubuntu 14.04 64x".
## Pull Requests
- **Add tests** - Your patch won't be accepted if it doesn't have tests where appropriate.
- **Document any change in behaviour** - Make sure the README and any other relevant documentation updated.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
- **Coding style** - Try to match the style of the rest of the source wherever possible. Your patch won't be accepted if the style is significantly different.

20
src/Label/vendor/php-ds/php-ds/LICENSE vendored Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 Rudi Theunissen
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,38 @@
# Data Structures for PHP
[![Build Status](https://github.com/php-ds/polyfill/workflows/CI/badge.svg)](https://github.com/php-ds/polyfill/actions?query=workflow%3A%22CI%22+branch%3Amaster)
[![Packagist](https://img.shields.io/packagist/v/php-ds/php-ds.svg)](https://packagist.org/packages/php-ds/php-ds)
This is a compatibility polyfill for the [extension](https://github.com/php-ds/extension). You should include this package as a dependency of your project
to ensure that your codebase would still be functional in an environment where the extension is not installed. The polyfill will not be loaded if the extension is installed and enabled.
## Install
```bash
composer require php-ds/php-ds
```
You can also *require* that the extension be installed using `ext-ds`.
## Test
```
composer install
composer test
```
Make sure that the *ds* extension is not enabled, as the polyfill will not be loaded if it is.
The test output will indicate whether the extension is active.
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for more information.
### Credits
- [Rudi Theunissen](https://github.com/rtheunissen)
- [Joe Watkins](https://github.com/krakjoe)
### License
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.

View File

@@ -0,0 +1,32 @@
{
"name": "php-ds/php-ds",
"license": "MIT",
"keywords": ["php", "ds", "data structures", "polyfill"],
"authors": [
{
"name": "Rudi Theunissen",
"email": "rudolf.theunissen@gmail.com"
}
],
"require": {
"php": ">=7.0",
"ext-json": "*"
},
"require-dev": {
"php-ds/tests": "^1.3"
},
"provide": {
"ext-ds": "1.3.0"
},
"suggest": {
"ext-ds": "to improve performance and reduce memory usage"
},
"scripts": {
"test": "phpunit"
},
"autoload": {
"psr-4" : {
"Ds\\": "src"
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Ds;
/**
* Collection is the base interface which covers functionality common to all the
* data structures in this library. It guarantees that all structures are
* traversable, countable, and can be converted to json using json_encode().
*
* @package Ds
*
* @template-covariant TKey
* @template-covariant TValue
* @extends Traversable<TKey, TValue>
*/
interface Collection extends \IteratorAggregate, \Countable, \JsonSerializable
{
/**
* Removes all values from the collection.
*/
public function clear();
/**
* Returns the size of the collection.
*
* @return int
*/
public function count(): int;
/**
* Returns a shallow copy of the collection.
*
* @return static a copy of the collection.
*
* @psalm-return static<TKey, TValue>
*/
public function copy();
/**
* Returns whether the collection is empty.
*
* This should be equivalent to a count of zero, but is not required.
* Implementations should define what empty means in their own context.
*/
public function isEmpty(): bool;
/**
* Returns an array representation of the collection.
*
* The format of the returned array is implementation-dependent.
* Some implementations may throw an exception if an array representation
* could not be created.
*
* @return array<TKey, TValue>
*/
public function toArray(): array;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Ds;
/**
* A Deque (pronounced "deck") is a sequence of values in a contiguous buffer
* that grows and shrinks automatically. The name is a common abbreviation of
* "double-ended queue".
*
* While a Deque is very similar to a Vector, it offers constant time operations
* at both ends of the buffer, ie. shift, unshift, push and pop are all O(1).
*
* @package Ds
*
* @template TValue
* @implements Sequence<TValue>
*/
final class Deque implements Sequence
{
use Traits\GenericCollection;
use Traits\GenericSequence;
use Traits\SquaredCapacity;
public const MIN_CAPACITY = 8;
protected function shouldIncreaseCapacity(): bool
{
return count($this) >= $this->capacity;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Ds;
/**
* Hashable is an interface which allows objects to be used as keys.
*
* Its an alternative to spl_object_hash(), which determines an objects hash
* based on its handle: this means that two objects that are considered equal
* by an implicit definition would not treated as equal because they are not
* the same instance.
*
* @package Ds
*/
interface Hashable
{
/**
* Produces a scalar value to be used as the object's hash, which determines
* where it goes in the hash table. While this value does not have to be
* unique, objects which are equal must have the same hash value.
*
* @return mixed
*/
public function hash();
/**
* Determines if two objects should be considered equal. Both objects will
* be instances of the same class but may not be the same instance.
*
* @param mixed $obj An instance of the same class to compare to.
*/
public function equals($obj): bool;
}

View File

@@ -0,0 +1,811 @@
<?php
namespace Ds;
use Error;
use OutOfBoundsException;
use OutOfRangeException;
use Traversable;
use UnderflowException;
/**
* A Map is a sequential collection of key-value pairs, almost identical to an
* array used in a similar context. Keys can be any type, but must be unique.
*
* @package Ds
*
* @template TKey
* @template TValue
* @implements Collection<TKey, TValue>
*/
final class Map implements Collection, \ArrayAccess
{
use Traits\GenericCollection;
use Traits\SquaredCapacity;
public const MIN_CAPACITY = 8;
/**
* @var array internal array to store pairs
*
* @psalm-var array<int, Pair>
*/
private $pairs = [];
/**
* Creates a new instance.
*
* @param iterable<mixed, mixed> $values
*
* @psalm-param iterable<TKey, TValue> $values
*/
public function __construct(iterable $values = [])
{
if (func_num_args()) {
$this->putAll($values);
}
}
/**
* Updates all values by applying a callback function to each value.
*
* @param callable $callback Accepts two arguments: key and value, should
* return what the updated value will be.
*
* @psalm-param callable(TKey, TValue): TValue $callback
*/
public function apply(callable $callback)
{
foreach ($this->pairs as &$pair) {
$pair->value = $callback($pair->key, $pair->value);
}
}
/**
* @inheritDoc
*/
public function clear()
{
$this->pairs = [];
$this->capacity = self::MIN_CAPACITY;
}
/**
* Return the first Pair from the Map
*
* @return Pair
*
* @throws UnderflowException
*
* @psalm-return Pair<TKey, TValue>
*/
public function first(): Pair
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->pairs[0];
}
/**
* Return the last Pair from the Map
*
* @return Pair
*
* @throws UnderflowException
*
* @psalm-return Pair<TKey, TValue>
*/
public function last(): Pair
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->pairs[count($this->pairs) - 1];
}
/**
* Return the pair at a specified position in the Map
*
* @return Pair
*
* @throws OutOfRangeException
*
* @psalm-return Pair<TKey, TValue>
*/
public function skip(int $position): Pair
{
if ($position < 0 || $position >= count($this->pairs)) {
throw new OutOfRangeException();
}
return $this->pairs[$position]->copy();
}
/**
* Returns the result of associating all keys of a given traversable object
* or array with their corresponding values, as well as those of this map.
*
* @param array|\Traversable $values
*
* @return Map
*
* @template TKey2
* @template TValue2
* @psalm-param iterable<TKey2, TValue2> $values
* @psalm-return Map<TKey|TKey2, TValue|TValue2>
*/
public function merge($values): Map
{
$merged = new self($this);
$merged->putAll($values);
return $merged;
}
/**
* Creates a new map containing the pairs of the current instance whose keys
* are also present in the given map. In other words, returns a copy of the
* current map with all keys removed that are not also in the other map.
*
* @param Map $map The other map.
*
* @return Map A new map containing the pairs of the current instance
* whose keys are also present in the given map. In other
* words, returns a copy of the current map with all keys
* removed that are not also in the other map.
*
* @template TKey2
* @template TValue2
* @psalm-param Map<TKey2, TValue2> $map
* @psalm-return Map<TKey&TKey2, TValue>
*/
public function intersect(Map $map): Map
{
return $this->filter(function($key) use ($map) {
return $map->hasKey($key);
});
}
/**
* Returns the result of removing all keys from the current instance that
* are present in a given map.
*
* @param Map $map The map containing the keys to exclude.
*
* @return Map The result of removing all keys from the current instance
* that are present in a given map.
*
* @template TValue2
* @psalm-param Map<TKey, TValue2> $map
* @psalm-return Map<TKey, TValue>
*/
public function diff(Map $map): Map
{
return $this->filter(function($key) use ($map) {
return ! $map->hasKey($key);
});
}
/**
* Determines whether two keys are equal.
*
* @param mixed $a
* @param mixed $b
*
* @psalm-param TKey $a
* @psalm-param TKey $b
*/
private function keysAreEqual($a, $b): bool
{
if (is_object($a) && $a instanceof Hashable) {
return get_class($a) === get_class($b) && $a->equals($b);
}
return $a === $b;
}
/**
* Attempts to look up a key in the table.
*
* @param $key
*
* @return Pair|null
*
* @psalm-return Pair<TKey, TValue>|null
*/
private function lookupKey($key)
{
foreach ($this->pairs as $pair) {
if ($this->keysAreEqual($pair->key, $key)) {
return $pair;
}
}
}
/**
* Attempts to look up a value in the table.
*
* @param $value
*
* @return Pair|null
*
* @psalm-return Pair<TKey, TValue>|null
*/
private function lookupValue($value)
{
foreach ($this->pairs as $pair) {
if ($pair->value === $value) {
return $pair;
}
}
}
/**
* Returns whether an association a given key exists.
*
* @param mixed $key
*
* @psalm-param TKey $key
*/
public function hasKey($key): bool
{
return $this->lookupKey($key) !== null;
}
/**
* Returns whether an association for a given value exists.
*
* @param mixed $value
*
* @psalm-param TValue $value
*/
public function hasValue($value): bool
{
return $this->lookupValue($value) !== null;
}
/**
* @inheritDoc
*/
public function count(): int
{
return count($this->pairs);
}
/**
* Returns a new map containing only the values for which a predicate
* returns true. A boolean test will be used if a predicate is not provided.
*
* @param callable|null $callback Accepts a key and a value, and returns:
* true : include the value,
* false: skip the value.
*
* @return Map
*
* @psalm-param (callable(TKey, TValue): bool)|null $callback
* @psalm-return Map<TKey, TValue>
*/
public function filter(callable $callback = null): Map
{
$filtered = new self();
foreach ($this as $key => $value) {
if ($callback ? $callback($key, $value) : $value) {
$filtered->put($key, $value);
}
}
return $filtered;
}
/**
* Returns the value associated with a key, or an optional default if the
* key is not associated with a value.
*
* @param mixed $key
* @param mixed $default
*
* @return mixed The associated value or fallback default if provided.
*
* @throws OutOfBoundsException if no default was provided and the key is
* not associated with a value.
*
* @template TDefault
* @psalm-param TKey $key
* @psalm-param TDefault $default
* @psalm-return TValue|TDefault
*/
public function get($key, $default = null)
{
if (($pair = $this->lookupKey($key))) {
return $pair->value;
}
// Check if a default was provided.
if (func_num_args() === 1) {
throw new OutOfBoundsException();
}
return $default;
}
/**
* Returns a set of all the keys in the map.
*
* @return Set
*
* @psalm-return Set<TKey>
*/
public function keys(): Set
{
$key = function($pair) {
return $pair->key;
};
return new Set(array_map($key, $this->pairs));
}
/**
* Returns a new map using the results of applying a callback to each value.
*
* The keys will be equal in both maps.
*
* @param callable $callback Accepts two arguments: key and value, should
* return what the updated value will be.
*
* @return Map
*
* @template TNewValue
* @psalm-param callable(TKey, TValue): TNewValue $callback
* @psalm-return Map<TKey, TNewValue>
*/
public function map(callable $callback): Map
{
$mapped = new self();
foreach ($this->pairs as $pair) {
$mapped->put($pair->key, $callback($pair->key, $pair->value));
}
return $mapped;
}
/**
* Returns a sequence of pairs representing all associations.
*
* @return Sequence
*
* @psalm-return Sequence<Pair<TKey, TValue>>
*/
public function pairs(): Sequence
{
$copy = function($pair) {
return $pair->copy();
};
return new Vector(array_map($copy, $this->pairs));
}
/**
* Associates a key with a value, replacing a previous association if there
* was one.
*
* @param mixed $key
* @param mixed $value
*
* @psalm-param TKey $key
* @psalm-param TValue $value
*/
public function put($key, $value)
{
$pair = $this->lookupKey($key);
if ($pair) {
$pair->value = $value;
} else {
$this->checkCapacity();
$this->pairs[] = new Pair($key, $value);
}
}
/**
* Creates associations for all keys and corresponding values of either an
* array or iterable object.
*
* @param iterable<mixed, mixed> $values
*
* @psalm-param iterable<TKey, TValue> $values
*/
public function putAll(iterable $values)
{
foreach ($values as $key => $value) {
$this->put($key, $value);
}
}
/**
* Iteratively reduces the map to a single value using a callback.
*
* @param callable $callback Accepts the carry, key, and value, and
* returns an updated carry value.
*
* @param mixed|null $initial Optional initial carry value.
*
* @return mixed The carry value of the final iteration, or the initial
* value if the map was empty.
*
* @template TCarry
* @psalm-param callable(TCarry, TKey, TValue): TCarry $callback
* @psalm-param TCarry $initial
* @psalm-return TCarry
*/
public function reduce(callable $callback, $initial = null)
{
$carry = $initial;
foreach ($this->pairs as $pair) {
$carry = $callback($carry, $pair->key, $pair->value);
}
return $carry;
}
/**
* Completely removes a pair from the internal array by position. It is
* important to remove it from the array and not just use 'unset'.
*
* @return mixed
*
* @psalm-return TValue
*/
private function delete(int $position)
{
$pair = $this->pairs[$position];
$value = $pair->value;
array_splice($this->pairs, $position, 1, null);
$this->checkCapacity();
return $value;
}
/**
* Removes a key's association from the map and returns the associated value
* or a provided default if provided.
*
* @param mixed $key
* @param mixed $default
*
* @return mixed The associated value or fallback default if provided.
*
* @throws \OutOfBoundsException if no default was provided and the key is
* not associated with a value.
*
* @template TDefault
* @psalm-param TKey $key
* @psalm-param TDefault $default
* @psalm-return TValue|TDefault
*/
public function remove($key, $default = null)
{
foreach ($this->pairs as $position => $pair) {
if ($this->keysAreEqual($pair->key, $key)) {
return $this->delete($position);
}
}
// Check if a default was provided
if (func_num_args() === 1) {
throw new \OutOfBoundsException();
}
return $default;
}
/**
* Reverses the map in-place
*/
public function reverse()
{
$this->pairs = array_reverse($this->pairs);
}
/**
* Returns a reversed copy of the map.
*
* @return Map
*
* @psalm-return Map<TKey, TValue>
*/
public function reversed(): Map
{
$reversed = new self();
$reversed->pairs = array_reverse($this->pairs);
return $reversed;
}
/**
* Returns a sub-sequence of a given length starting at a specified offset.
*
* @param int $offset If the offset is non-negative, the map will
* start at that offset in the map. If offset is
* negative, the map will start that far from the
* end.
*
* @param int|null $length If a length is given and is positive, the
* resulting set will have up to that many pairs in
* it. If the requested length results in an
* overflow, only pairs up to the end of the map
* will be included.
*
* If a length is given and is negative, the map
* will stop that many pairs from the end.
*
* If a length is not provided, the resulting map
* will contains all pairs between the offset and
* the end of the map.
*
* @return Map
*
* @psalm-return Map<TKey, TValue>
*/
public function slice(int $offset, int $length = null): Map
{
$map = new self();
if (func_num_args() === 1) {
$slice = array_slice($this->pairs, $offset);
} else {
$slice = array_slice($this->pairs, $offset, $length);
}
foreach ($slice as $pair) {
$map->put($pair->key, $pair->value);
}
return $map;
}
/**
* Sorts the map in-place, based on an optional callable comparator.
*
* The map will be sorted by value.
*
* @param callable|null $comparator Accepts two values to be compared.
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
*/
public function sort(callable $comparator = null)
{
if ($comparator) {
usort($this->pairs, function($a, $b) use ($comparator) {
return $comparator($a->value, $b->value);
});
} else {
usort($this->pairs, function($a, $b) {
return $a->value <=> $b->value;
});
}
}
/**
* Returns a sorted copy of the map, based on an optional callable
* comparator. The map will be sorted by value.
*
* @param callable|null $comparator Accepts two values to be compared.
*
* @return Map
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
* @psalm-return Map<TKey, TValue>
*/
public function sorted(callable $comparator = null): Map
{
$copy = $this->copy();
$copy->sort($comparator);
return $copy;
}
/**
* Sorts the map in-place, based on an optional callable comparator.
*
* The map will be sorted by key.
*
* @param callable|null $comparator Accepts two keys to be compared.
*
* @psalm-param (callable(TKey, TKey): int)|null $comparator
*/
public function ksort(callable $comparator = null)
{
if ($comparator) {
usort($this->pairs, function($a, $b) use ($comparator) {
return $comparator($a->key, $b->key);
});
} else {
usort($this->pairs, function($a, $b) {
return $a->key <=> $b->key;
});
}
}
/**
* Returns a sorted copy of the map, based on an optional callable
* comparator. The map will be sorted by key.
*
* @param callable|null $comparator Accepts two keys to be compared.
*
* @return Map
*
* @psalm-param (callable(TKey, TKey): int)|null $comparator
* @psalm-return Map<TKey, TValue>
*/
public function ksorted(callable $comparator = null): Map
{
$copy = $this->copy();
$copy->ksort($comparator);
return $copy;
}
/**
* Returns the sum of all values in the map.
*
* @return int|float The sum of all the values in the map.
*/
public function sum()
{
return $this->values()->sum();
}
/**
* @inheritDoc
*/
public function toArray(): array
{
$array = [];
foreach ($this->pairs as $pair) {
$array[$pair->key] = $pair->value;
}
return $array;
}
/**
* Returns a sequence of all the associated values in the Map.
*
* @return Sequence
*
* @psalm-return Sequence<TValue>
*/
public function values(): Sequence
{
$value = function($pair) {
return $pair->value;
};
return new Vector(array_map($value, $this->pairs));
}
/**
* Creates a new map that contains the pairs of the current instance as well
* as the pairs of another map.
*
* @param Map $map The other map, to combine with the current instance.
*
* @return Map A new map containing all the pairs of the current
* instance as well as another map.
*
* @template TKey2
* @template TValue2
* @psalm-param Map<TKey2, TValue2> $map
* @psalm-return Map<TKey|TKey2, TValue|TValue2>
*/
public function union(Map $map): Map
{
return $this->merge($map);
}
/**
* Creates a new map using keys of either the current instance or of another
* map, but not of both.
*
* @param Map $map
*
* @return Map A new map containing keys in the current instance as well
* as another map, but not in both.
*
* @template TKey2
* @template TValue2
* @psalm-param Map<TKey2, TValue2> $map
* @psalm-return Map<TKey|TKey2, TValue|TValue2>
*/
public function xor(Map $map): Map
{
return $this->merge($map)->filter(function($key) use ($map) {
return $this->hasKey($key) ^ $map->hasKey($key);
});
}
/**
* @inheritDoc
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->pairs as $pair) {
yield $pair->key => $pair->value;
}
}
/**
* Returns a representation to be used for var_dump and print_r.
*
* @psalm-return array<Pair<TKey, TValue>>
*/
public function __debugInfo()
{
return $this->pairs()->toArray();
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
$this->put($offset, $value);
}
/**
* @inheritdoc
*
* @throws OutOfBoundsException
*/
#[\ReturnTypeWillChange]
public function &offsetGet($offset)
{
$pair = $this->lookupKey($offset);
if ($pair) {
return $pair->value;
}
throw new OutOfBoundsException();
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
$this->remove($offset, null);
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return $this->get($offset, null) !== null;
}
/**
* Returns a representation that can be natively converted to JSON, which is
* called when invoking json_encode.
*
* @return mixed
*
* @see \JsonSerializable
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return (object) $this->toArray();
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Ds;
use OutOfBoundsException;
/**
* A pair which represents a key and an associated value.
*
* @property mixed $key
* @property mixed $value
*
* @package Ds
*
* @template-covariant TKey
* @template-covariant TValue
*/
final class Pair implements \JsonSerializable
{
/**
* @var mixed The pair's key
*
* @psalm-param TKey $key
*/
public $key;
/**
* @var mixed The pair's value
*
* @psalm-param TValue $value
*/
public $value;
/**
* Creates a new instance.
*
* @param mixed $key
* @param mixed $value
*
* @psalm-param TKey $key
* @psalm-param TValue $value
*/
public function __construct($key = null, $value = null)
{
$this->key = $key;
$this->value = $value;
}
/**
*
* @param mixed $name
*
* @return mixed|null
*/
public function __isset($name)
{
if ($name === 'key' || $name === 'value') {
return $this->$name !== null;
}
return false;
}
/**
* This allows unset($pair->key) to not completely remove the property,
* but be set to null instead.
*
* @return void
*/
public function __unset(string $name)
{
if ($name === 'key' || $name === 'value') {
$this->$name = null;
return;
}
throw new OutOfBoundsException();
}
/**
* @param mixed $name
*
* @return mixed|null
*/
public function &__get($name)
{
if ($name === 'key' || $name === 'value') {
return $this->$name;
}
throw new OutOfBoundsException();
}
/**
* @param mixed $name
* @param mixed $value
*
* @return mixed|null
*/
public function __set($name, $value)
{
if ($name === 'key' || $name === 'value') {
$this->$name = $value;
return;
}
throw new OutOfBoundsException();
}
/**
* Returns a copy of the Pair
*
* @psalm-return self<TKey, TValue>
*/
public function copy(): self
{
return new self($this->key, $this->value);
}
/**
* Returns a representation to be used for var_dump and print_r.
*
* @return array
*
* @psalm-return array{key: TKey, value: TValue}
*/
public function __debugInfo()
{
return $this->toArray();
}
/**
* @inheritDoc
*
* @psalm-return array{key: TKey, value: TValue}
*/
public function toArray(): array
{
return [
'key' => $this->key,
'value' => $this->value,
];
}
/**
* @inheritDoc
*
* @psalm-return array{key: TKey, value: TValue}
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Returns a string representation of the pair.
*/
public function __toString()
{
return 'object(' . get_class($this) . ')';
}
}

View File

@@ -0,0 +1,340 @@
<?php
namespace Ds;
use UnderflowException;
/**
* A PriorityQueue is very similar to a Queue. Values are pushed into the queue
* with an assigned priority, and the value with the highest priority will
* always be at the front of the queue.
*
* @package Ds
*
* @template TValue
* @implements Collection<int, TValue>
*/
final class PriorityQueue implements Collection
{
use Traits\GenericCollection;
use Traits\SquaredCapacity;
public const MIN_CAPACITY = 8;
/**
* @var array<int, PriorityNode<TValue>>
*/
private $heap = [];
/**
* @var int
*/
private $stamp = 0;
/**
* Creates a new instance.
*/
public function __construct()
{
}
/**
* @inheritDoc
*/
public function clear()
{
$this->heap = [];
$this->stamp = 0;
$this->capacity = self::MIN_CAPACITY;
}
/**
* @inheritDoc
*/
public function copy(): self
{
$copy = new PriorityQueue();
$copy->heap = $this->heap;
$copy->stamp = $this->stamp;
$copy->capacity = $this->capacity;
return $copy;
}
/**
* @inheritDoc
*/
public function count(): int
{
return count($this->heap);
}
/**
* Returns the value with the highest priority in the priority queue.
*
* @return mixed
*
* @throw UnderflowException
*
* @psalm-return TValue
*/
public function peek()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->heap[0]->value;
}
/**
* Returns the index of a node's left leaf.
*
* @param int $index The index of the node.
*
* @return int The index of the left leaf.
*/
private function left(int $index): int
{
return ($index * 2) + 1;
}
/**
* Returns the index of a node's right leaf.
*
* @param int $index The index of the node.
*
* @return int The index of the right leaf.
*/
private function right(int $index): int
{
return ($index * 2) + 2;
}
/**
* Returns the index of a node's parent node.
*
* @param int $index The index of the node.
*
* @return int The index of the parent.
*/
private function parent(int $index): int
{
return (int) (($index - 1) / 2);
}
/**
* Compares two indices of the heap.
*
* @return int
*/
private function compare(int $a, int $b)
{
$x = $this->heap[$a];
$y = $this->heap[$b];
// Compare priority, using insertion stamp as fallback.
return ($x->priority <=> $y->priority) ?: ($y->stamp <=> $x->stamp);
}
/**
* Swaps the nodes at two indices of the heap.
*/
private function swap(int $a, int $b)
{
$temp = $this->heap[$a];
$this->heap[$a] = $this->heap[$b];
$this->heap[$b] = $temp;
}
/**
* Returns the index of a node's largest leaf node.
*
* @param int $parent the parent node.
*
* @return int the index of the node's largest leaf node.
*/
private function getLargestLeaf(int $parent)
{
$left = $this->left($parent);
$right = $this->right($parent);
if ($right < count($this->heap) && $this->compare($left, $right) < 0) {
return $right;
}
return $left;
}
/**
* Starts the process of sifting down a given node index to ensure that
* the heap's properties are preserved.
*/
private function siftDown(int $node)
{
$last = floor(count($this->heap) / 2);
for ($parent = $node; $parent < $last; $parent = $leaf) {
// Determine the largest leaf to potentially swap with the parent.
$leaf = $this->getLargestLeaf($parent);
// Done if the parent is not greater than its largest leaf
if ($this->compare($parent, $leaf) > 0) {
break;
}
$this->swap($parent, $leaf);
}
}
/**
* Sets the root node and sifts it down the heap.
*
* @param PriorityNode $node
*/
private function setRoot(PriorityNode $node)
{
$this->heap[0] = $node;
$this->siftDown(0);
}
/**
* Returns the root node of the heap.
*
* @return PriorityNode
*/
private function getRoot(): PriorityNode
{
return $this->heap[0];
}
/**
* Returns and removes the value with the highest priority in the queue.
*
* @return mixed
*
* @psalm-return TValue
*/
public function pop()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
// Last leaf of the heap to become the new root.
$leaf = array_pop($this->heap);
if (empty($this->heap)) {
return $leaf->value;
}
// Cache the current root value to return before replacing with next.
$value = $this->getRoot()->value;
// Replace the root, then sift down.
$this->setRoot($leaf);
$this->checkCapacity();
return $value;
}
/**
* Sifts a node up the heap until it's in the right position.
*/
private function siftUp(int $leaf)
{
for (; $leaf > 0; $leaf = $parent) {
$parent = $this->parent($leaf);
// Done when parent priority is greater.
if ($this->compare($leaf, $parent) < 0) {
break;
}
$this->swap($parent, $leaf);
}
}
/**
* Pushes a value into the queue, with a specified priority.
*
* @param mixed $value
*
* @psalm-param TValue $value
*/
public function push($value, int $priority)
{
$this->checkCapacity();
// Add new leaf, then sift up to maintain heap,
$this->heap[] = new PriorityNode($value, $priority, $this->stamp++);
$this->siftUp(count($this->heap) - 1);
}
/**
* @inheritDoc
*/
public function toArray(): array
{
$heap = $this->heap;
$array = [];
while ( ! $this->isEmpty()) {
$array[] = $this->pop();
}
$this->heap = $heap;
return $array;
}
/**
* @inheritDoc
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
while ( ! $this->isEmpty()) {
yield $this->pop();
}
}
}
/**
* @internal
*
* @template TValue
*/
final class PriorityNode
{
/**
* @var mixed
*
* @psalm-var TValue
*/
public $value;
/**
* @var int
*/
public $priority;
/**
* @var int
*/
public $stamp;
/**
* @param mixed $value
* @param int $priority
* @param int $stamp
*
* @psalm-param TValue $value
*/
public function __construct($value, int $priority, int $stamp)
{
$this->value = $value;
$this->priority = $priority;
$this->stamp = $stamp;
}
}

View File

@@ -0,0 +1,195 @@
<?php
namespace Ds;
use Error;
use OutOfBoundsException;
/**
* A “first in, first out” or “FIFO” collection that only allows access to the
* value at the front of the queue and iterates in that order, destructively.
*
* @package Ds
*
* @template TValue
* @implements Collection<int, TValue>
*/
final class Queue implements Collection, \ArrayAccess
{
use Traits\GenericCollection;
/**
* @var Deque internal deque to store values.
*
* @psalm-var Deque<TValue>
*/
private $deque;
/**
* Creates an instance using the values of an array or Traversable object.
*
* @param iterable<mixed> $values
*
* @psalm-param iterable<TValue> $values
*/
public function __construct(iterable $values = [])
{
$this->deque = new Deque($values);
}
/**
* Ensures that enough memory is allocated for a specified capacity. This
* potentially reduces the number of reallocations as the size increases.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity)
{
$this->deque->allocate($capacity);
}
/**
* Returns the current capacity of the queue.
*/
public function capacity(): int
{
return $this->deque->capacity();
}
/**
* @inheritDoc
*/
public function clear()
{
$this->deque->clear();
}
/**
* @inheritDoc
*/
public function copy(): self
{
return new self($this->deque);
}
/**
* @inheritDoc
*/
public function count(): int
{
return count($this->deque);
}
/**
* Returns the value at the front of the queue without removing it.
*
* @return mixed
*
* @psalm-return TValue
*/
public function peek()
{
return $this->deque->first();
}
/**
* Returns and removes the value at the front of the Queue.
*
* @return mixed
*
* @psalm-return TValue
*/
public function pop()
{
return $this->deque->shift();
}
/**
* Pushes zero or more values into the back of the queue.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function push(...$values)
{
$this->deque->push(...$values);
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return $this->deque->toArray();
}
/**
* Get iterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
while ( ! $this->isEmpty()) {
yield $this->pop();
}
}
/**
* @inheritdoc
*
* @throws OutOfBoundsException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->push($value);
} else {
throw new Error();
}
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
throw new Error();
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new Error();
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
throw new Error();
}
/**
* Ensures that the internal sequence will be cloned too.
*/
public function __clone()
{
$this->deque = clone $this->deque;
}
}

View File

@@ -0,0 +1,330 @@
<?php
namespace Ds;
/**
* Describes the behaviour of values arranged in a single, linear dimension.
* Some languages refer to this as a "List". Its similar to an array that uses
* incremental integer keys, with the exception of a few characteristics:
*
* - Values will always be indexed as [0, 1, 2, …, size - 1].
* - Only allowed to access values by index in the range [0, size - 1].
*
* @package Ds
*
* @template TValue
* @extends Collection<int, TValue>
*/
interface Sequence extends Collection, \ArrayAccess
{
/**
* Ensures that enough memory is allocated for a required capacity.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity);
/**
* Updates every value in the sequence by applying a callback, using the
* return value as the new value.
*
* @param callable $callback Accepts the value, returns the new value.
*
* @psalm-param callable(TValue): TValue $callback
*/
public function apply(callable $callback);
/**
* Returns the current capacity of the sequence.
*
* @return int
*/
public function capacity(): int;
/**
* Determines whether the sequence contains all of zero or more values.
*
* @param mixed ...$values
*
* @return bool true if at least one value was provided and the sequence
* contains all given values, false otherwise.
*
* @psalm-param TValue ...$values
*/
public function contains(...$values): bool;
/**
* Returns a new sequence containing only the values for which a callback
* returns true. A boolean test will be used if a callback is not provided.
*
* @param callable|null $callback Accepts a value, returns a boolean result:
* true : include the value,
* false: skip the value.
*
* @return Sequence
*
* @psalm-param (callable(TValue): bool)|null $callback
* @psalm-return Sequence<TValue>
*/
public function filter(callable $callback = null): Sequence;
/**
* Returns the index of a given value, or null if it could not be found.
*
* @param mixed $value
*
* @return int|null
*
* @psalm-param TValue $value
*/
public function find($value);
/**
* Returns the first value in the sequence.
*
* @return mixed
*
* @throws \UnderflowException if the sequence is empty.
*
* @psalm-return TValue
*/
public function first();
/**
* Returns the value at a given index (position) in the sequence.
*
* @return mixed
*
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-return TValue
*/
public function get(int $index);
/**
* Inserts zero or more values at a given index.
*
* Each value after the index will be moved one position to the right.
* Values may be inserted at an index equal to the size of the sequence.
*
* @param mixed ...$values
*
* @throws \OutOfRangeException if the index is not in the range [0, n]
*
* @psalm-param TValue ...$values
*/
public function insert(int $index, ...$values);
/**
* Joins all values of the sequence into a string, adding an optional 'glue'
* between them. Returns an empty string if the sequence is empty.
*/
public function join(string $glue = null): string;
/**
* Returns the last value in the sequence.
*
* @return mixed
*
* @throws \UnderflowException if the sequence is empty.
*
* @psalm-return TValue
*/
public function last();
/**
* Returns a new sequence using the results of applying a callback to each
* value.
*
* @param callable $callback
*
* @return Sequence
*
* @template TNewValue
* @psalm-param callable(TValue): TNewValue $callback
* @psalm-return Sequence<TNewValue>
*/
public function map(callable $callback): Sequence;
/**
* Returns the result of adding all given values to the sequence.
*
* @param array|\Traversable $values
*
* @return Sequence
*
* @template TValue2
* @psalm-param iterable<TValue2> $values
* @psalm-return Sequence<TValue|TValue2>
*/
public function merge($values): Sequence;
/**
* Removes the last value in the sequence, and returns it.
*
* @return mixed what was the last value in the sequence.
*
* @throws \UnderflowException if the sequence is empty.
*
* @psalm-return TValue
*/
public function pop();
/**
* Adds zero or more values to the end of the sequence.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function push(...$values);
/**
* Iteratively reduces the sequence to a single value using a callback.
*
* @param callable $callback Accepts the carry and current value, and
* returns an updated carry value.
*
* @param mixed|null $initial Optional initial carry value.
*
* @return mixed The carry value of the final iteration, or the initial
* value if the sequence was empty.
*
* @template TCarry
* @psalm-param callable(TCarry, TValue): TCarry $callback
* @psalm-param TCarry $initial
* @psalm-return TCarry
*/
public function reduce(callable $callback, $initial = null);
/**
* Removes and returns the value at a given index in the sequence.
*
* @param int $index this index to remove.
*
* @return mixed the removed value.
*
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-return TValue
*/
public function remove(int $index);
/**
* Reverses the sequence in-place.
*/
public function reverse();
/**
* Returns a reversed copy of the sequence.
*
* @return Sequence
*
* @psalm-return Sequence<TValue>
*/
public function reversed();
/**
* Rotates the sequence by a given number of rotations, which is equivalent
* to successive calls to 'shift' and 'push' if the number of rotations is
* positive, or 'pop' and 'unshift' if negative.
*
* @param int $rotations The number of rotations (can be negative).
*/
public function rotate(int $rotations);
/**
* Replaces the value at a given index in the sequence with a new value.
*
* @param mixed $value
*
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-param TValue $value
*/
public function set(int $index, $value);
/**
* Removes and returns the first value in the sequence.
*
* @return mixed what was the first value in the sequence.
*
* @throws \UnderflowException if the sequence was empty.
*
* @psalm-return TValue
*/
public function shift();
/**
* Returns a sub-sequence of a given length starting at a specified index.
*
* @param int $index If the index is positive, the sequence will start
* at that index in the sequence. If index is negative,
* the sequence will start that far from the end.
*
* @param int $length If a length is given and is positive, the resulting
* sequence will have up to that many values in it.
* If the length results in an overflow, only values
* up to the end of the sequence will be included.
*
* If a length is given and is negative, the sequence
* will stop that many values from the end.
*
* If a length is not provided, the resulting sequence
* will contain all values between the index and the
* end of the sequence.
*
* @return Sequence
*
* @psalm-return Sequence<TValue>
*/
public function slice(int $index, int $length = null): Sequence;
/**
* Sorts the sequence in-place, based on an optional callable comparator.
*
* @param callable|null $comparator Accepts two values to be compared.
* Should return the result of a <=> b.
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
*/
public function sort(callable $comparator = null);
/**
* Returns a sorted copy of the sequence, based on an optional callable
* comparator. Natural ordering will be used if a comparator is not given.
*
* @param callable|null $comparator Accepts two values to be compared.
* Should return the result of a <=> b.
*
* @return Sequence
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
* @psalm-return Sequence<TValue>
*/
public function sorted(callable $comparator = null): Sequence;
/**
* Returns the sum of all values in the sequence.
*
* @return int|float The sum of all the values in the sequence.
*/
public function sum();
/**
* @inheritDoc
*
* @return list<TValue>
*/
function toArray(): array;
/**
* Adds zero or more values to the front of the sequence.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function unshift(...$values);
}

View File

@@ -0,0 +1,538 @@
<?php
namespace Ds;
use Error;
use OutOfBoundsException;
use OutOfRangeException;
/**
* A sequence of unique values.
*
* @package Ds
*
* @template TValue
* @implements Collection<int, TValue>
*/
final class Set implements Collection, \ArrayAccess
{
use Traits\GenericCollection;
public const MIN_CAPACITY = Map::MIN_CAPACITY;
/**
* @var Map internal map to store the values.
*
* @psalm-var Map<int, TValue>
*/
private $table;
/**
* Creates a new set using the values of an array or Traversable object.
* The keys of either will not be preserved.
*
* @param iterable $values
*
* @psalm-param iterable<TValue> $values
*/
public function __construct(iterable $values = [])
{
$this->table = new Map();
foreach ($values as $value) {
$this->add($value);
}
}
/**
* Adds zero or more values to the set.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function add(...$values)
{
foreach ($values as $value) {
$this->table->put($value, null);
}
}
/**
* Ensures that enough memory is allocated for a specified capacity. This
* potentially reduces the number of reallocations as the size increases.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity)
{
$this->table->allocate($capacity);
}
/**
* Returns the current capacity of the set.
*/
public function capacity(): int
{
return $this->table->capacity();
}
/**
* Clear all elements in the Set
*/
public function clear()
{
$this->table->clear();
}
/**
* Determines whether the set contains all of zero or more values.
*
* @param mixed ...$values
*
* @return bool true if at least one value was provided and the set
* contains all given values, false otherwise.
*
* @psalm-param TValue ...$values
*/
public function contains(...$values): bool
{
foreach ($values as $value) {
if ( ! $this->table->hasKey($value)) {
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function copy(): self
{
return new self($this);
}
/**
* Returns the number of elements in the Stack
*
* @return int
*/
public function count(): int
{
return count($this->table);
}
/**
* Creates a new set using values from this set that aren't in another set.
*
* Formally: A \ B = {x ∈ A | x ∉ B}
*
* @param Set $set
*
* @return Set
*
* @template TValue2
* @psalm-param Set<TValue2> $set
* @psalm-return Set<TValue>
*/
public function diff(Set $set): Set
{
return $this->table->diff($set->table)->keys();
}
/**
* Creates a new set using values in either this set or in another set,
* but not in both.
*
* Formally: A ⊖ B = {x : x ∈ (A \ B) (B \ A)}
*
* @param Set $set
*
* @return Set
*
* @template TValue2
* @psalm-param Set<TValue2> $set
* @psalm-return Set<TValue|TValue2>
*/
public function xor(Set $set): Set
{
return $this->table->xor($set->table)->keys();
}
/**
* Returns a new set containing only the values for which a callback
* returns true. A boolean test will be used if a callback is not provided.
*
* @param callable|null $callback Accepts a value, returns a boolean:
* true : include the value,
* false: skip the value.
*
* @return Set
*
* @psalm-param (callable(TValue): bool)|null $callback
* @psalm-return Set<TValue>
*/
public function filter(callable $callback = null): Set
{
return new self(array_filter($this->toArray(), $callback ?: 'boolval'));
}
/**
* Returns the first value in the set.
*
* @return mixed the first value in the set.
*
* @psalm-return TValue
*/
public function first()
{
return $this->table->first()->key;
}
/**
* Returns the value at a specified position in the set.
*
* @return mixed|null
*
* @throws OutOfRangeException
*
* @psalm-return TValue
*/
public function get(int $position)
{
return $this->table->skip($position)->key;
}
/**
* Creates a new set using values common to both this set and another set.
*
* In other words, returns a copy of this set with all values removed that
* aren't in the other set.
*
* Formally: A ∩ B = {x : x ∈ A ∧ x ∈ B}
*
* @param Set $set
*
* @return Set
*
* @template TValue2
* @psalm-param Set<TValue2> $set
* @psalm-return Set<TValue&TValue2>
*/
public function intersect(Set $set): Set
{
return $this->table->intersect($set->table)->keys();
}
/**
* @inheritDoc
*/
public function isEmpty(): bool
{
return $this->table->isEmpty();
}
/**
* Joins all values of the set into a string, adding an optional 'glue'
* between them. Returns an empty string if the set is empty.
*
* @param string|null $glue
*/
public function join(string $glue = null): string
{
return implode($glue ?? '', $this->toArray());
}
/**
* Returns the last value in the set.
*
* @return mixed the last value in the set.
*
* @psalm-return TValue
*/
public function last()
{
return $this->table->last()->key;
}
/**
* Returns a new set using the results of applying a callback to each
* value.
*
* @param callable $callback
*
* @return Set
*
* @template TNewValue
* @psalm-param callable(TValue): TNewValue $callback
* @psalm-return Set<TNewValue>
*/
public function map(callable $callback) {
return new self(array_map($callback, $this->toArray()));
}
/**
* Iteratively reduces the set to a single value using a callback.
*
* @param callable $callback Accepts the carry and current value, and
* returns an updated carry value.
*
* @param mixed|null $initial Optional initial carry value.
*
* @return mixed The carry value of the final iteration, or the initial
* value if the set was empty.
*
* @template TCarry
* @psalm-param callable(TCarry, TValue): TCarry $callback
* @psalm-param TCarry $initial
* @psalm-return TCarry
*/
public function reduce(callable $callback, $initial = null)
{
$carry = $initial;
foreach ($this as $value) {
$carry = $callback($carry, $value);
}
return $carry;
}
/**
* Removes zero or more values from the set.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function remove(...$values)
{
foreach ($values as $value) {
$this->table->remove($value, null);
}
}
/**
* Reverses the set in-place.
*/
public function reverse()
{
$this->table->reverse();
}
/**
* Returns a reversed copy of the set.
*
* @return Set
*
* @psalm-return Set<TValue>
*/
public function reversed(): Set
{
$reversed = $this->copy();
$reversed->table->reverse();
return $reversed;
}
/**
* Returns a subset of a given length starting at a specified offset.
*
* @param int $offset If the offset is non-negative, the set will start
* at that offset in the set. If offset is negative,
* the set will start that far from the end.
*
* @param int $length If a length is given and is positive, the resulting
* set will have up to that many values in it.
* If the requested length results in an overflow, only
* values up to the end of the set will be included.
*
* If a length is given and is negative, the set
* will stop that many values from the end.
*
* If a length is not provided, the resulting set
* will contains all values between the offset and the
* end of the set.
*
* @return Set
*
* @psalm-return Set<TValue>
*/
public function slice(int $offset, int $length = null): Set
{
$sliced = new self();
$sliced->table = $this->table->slice($offset, $length);
return $sliced;
}
/**
* Sorts the set in-place, based on an optional callable comparator.
*
* @param callable|null $comparator Accepts two values to be compared.
* Should return the result of a <=> b.
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
*/
public function sort(callable $comparator = null)
{
$this->table->ksort($comparator);
}
/**
* Returns a sorted copy of the set, based on an optional callable
* comparator. Natural ordering will be used if a comparator is not given.
*
* @param callable|null $comparator Accepts two values to be compared.
* Should return the result of a <=> b.
*
* @return Set
*
* @psalm-param (callable(TValue, TValue): int)|null $comparator
* @psalm-return Set<TValue>
*/
public function sorted(callable $comparator = null): Set
{
$sorted = $this->copy();
$sorted->table->ksort($comparator);
return $sorted;
}
/**
* Returns the result of adding all given values to the set.
*
* @param array|\Traversable $values
*
* @return Set
*
* @template TValue2
* @psalm-param iterable<TValue2> $values
* @psalm-return Set<TValue|TValue2>
*/
public function merge($values): Set
{
$merged = $this->copy();
foreach ($values as $value) {
$merged->add($value);
}
return $merged;
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return iterator_to_array($this);
}
/**
* Returns the sum of all values in the set.
*
* @return int|float The sum of all the values in the set.
*/
public function sum()
{
return array_sum($this->toArray());
}
/**
* Creates a new set that contains the values of this set as well as the
* values of another set.
*
* Formally: A B = {x: x ∈ A x ∈ B}
*
* @param Set $set
*
* @return Set
*
* @template TValue2
* @psalm-param Set<TValue2> $set
* @psalm-return Set<TValue|TValue2>
*/
public function union(Set $set): Set
{
$union = new self();
foreach ($this as $value) {
$union->add($value);
}
foreach ($set as $value) {
$union->add($value);
}
return $union;
}
/**
* Get iterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->table as $key => $value) {
yield $key;
}
}
/**
* @inheritdoc
*
* @throws OutOfBoundsException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->add($value);
return;
}
throw new Error();
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->table->skip($offset)->key;
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
throw new Error();
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new Error();
}
/**
* Ensures that the internal table will be cloned too.
*/
public function __clone()
{
$this->table = clone $this->table;
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Ds;
use Error;
use OutOfBoundsException;
/**
* A “last in, first out” or “LIFO” collection that only allows access to the
* value at the top of the structure and iterates in that order, destructively.
*
* @package Ds
*
* @template TValue
* @implements Sequence<TValue>
*/
final class Stack implements Collection, \ArrayAccess
{
use Traits\GenericCollection;
/**
* @var Vector internal vector to store values of the stack.
*
* @psalm-var Vector<TValue>
*/
private $vector;
/**
* Creates an instance using the values of an array or Traversable object.
*
* @param iterable<mixed> $values
*
* @psalm-param iterable<TValue> $values
*/
public function __construct(iterable $values = [])
{
$this->vector = new Vector($values);
}
/**
* Clear all elements in the Stack
*/
public function clear()
{
$this->vector->clear();
}
/**
* @inheritdoc
*/
public function copy(): self
{
return new self($this->vector);
}
/**
* Returns the number of elements in the Stack
*/
public function count(): int
{
return count($this->vector);
}
/**
* Ensures that enough memory is allocated for a specified capacity. This
* potentially reduces the number of reallocations as the size increases.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity)
{
$this->vector->allocate($capacity);
}
/**
* Returns the current capacity of the stack.
*/
public function capacity(): int
{
return $this->vector->capacity();
}
/**
* Returns the value at the top of the stack without removing it.
*
* @return mixed
*
* @throws \UnderflowException if the stack is empty.
*
* @psalm-return TValue
*/
public function peek()
{
return $this->vector->last();
}
/**
* Returns and removes the value at the top of the stack.
*
* @return mixed
*
* @throws \UnderflowException if the stack is empty.
*
* @psalm-return TValue
*/
public function pop()
{
return $this->vector->pop();
}
/**
* Pushes zero or more values onto the top of the stack.
*
* @param mixed ...$values
*
* @psalm-param TValue ...$values
*/
public function push(...$values)
{
$this->vector->push(...$values);
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return array_reverse($this->vector->toArray());
}
/**
*
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
while ( ! $this->isEmpty()) {
yield $this->pop();
}
}
/**
* @inheritdoc
*
* @throws OutOfBoundsException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->push($value);
} else {
throw new Error();
}
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
throw new Error();
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new Error();
}
/**
* @inheritdoc
*
* @throws Error
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
throw new Error();
}
/**
* Ensures that the internal vector will be cloned too.
*/
public function __clone()
{
$this->vector = clone $this->vector;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Ds\Traits;
use Ds\Deque;
/**
* Common to structures that deal with an internal capacity. While none of the
* PHP implementations actually make use of a capacity, it's important to keep
* consistent with the extension.
*/
trait Capacity
{
/**
* @var int internal capacity
*/
private $capacity = self::MIN_CAPACITY;
/**
* Returns the current capacity.
*/
public function capacity(): int
{
return $this->capacity;
}
/**
* Ensures that enough memory is allocated for a specified capacity. This
* potentially reduces the number of reallocations as the size increases.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity)
{
$this->capacity = max($capacity, $this->capacity);
}
/**
* @return float the structures growth factor.
*/
protected function getGrowthFactor(): float
{
return 2;
}
/**
* @return float to multiply by when decreasing capacity.
*/
protected function getDecayFactor(): float
{
return 0.5;
}
/**
* @return float the ratio between size and capacity when capacity should be
* decreased.
*/
protected function getTruncateThreshold(): float
{
return 0.25;
}
/**
* Checks and adjusts capacity if required.
*/
protected function checkCapacity()
{
if ($this->shouldIncreaseCapacity()) {
$this->increaseCapacity();
} else {
if ($this->shouldDecreaseCapacity()) {
$this->decreaseCapacity();
}
}
}
/**
* @param int $total
*/
protected function ensureCapacity(int $total)
{
if ($total > $this->capacity()) {
$this->capacity = max($total, $this->nextCapacity());
}
}
/**
* @return bool whether capacity should be increased.
*/
protected function shouldIncreaseCapacity(): bool
{
return $this->count() >= $this->capacity();
}
protected function nextCapacity(): int
{
return (int) ($this->capacity() * $this->getGrowthFactor());
}
/**
* Called when capacity should be increased to accommodate new values.
*/
protected function increaseCapacity()
{
$this->capacity = max(
$this->count(),
$this->nextCapacity()
);
}
/**
* Called when capacity should be decrease if it drops below a threshold.
*/
protected function decreaseCapacity()
{
$this->capacity = max(
self::MIN_CAPACITY,
(int) ($this->capacity() * $this->getDecayFactor())
);
}
/**
* @return bool whether capacity should be increased.
*/
protected function shouldDecreaseCapacity(): bool
{
return count($this) <= $this->capacity() * $this->getTruncateThreshold();
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Ds\Traits;
/**
* Common to structures that implement the base collection interface.
*/
trait GenericCollection
{
/**
* Returns whether the collection is empty.
*
* This should be equivalent to a count of zero, but is not required.
* Implementations should define what empty means in their own context.
*
* @return bool whether the collection is empty.
*/
public function isEmpty(): bool
{
return count($this) === 0;
}
/**
* Returns a representation that can be natively converted to JSON, which is
* called when invoking json_encode.
*
* @return mixed
*
* @see \JsonSerializable
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Creates a shallow copy of the collection.
*
* @return static a shallow copy of the collection.
*/
public function copy(): self
{
return new static($this);
}
/**
* Returns an array representation of the collection.
*
* The format of the returned array is implementation-dependent. Some
* implementations may throw an exception if an array representation
* could not be created (for example when object are used as keys).
*
* @return array
*/
abstract public function toArray(): array;
/**
* Invoked when calling var_dump.
*
* @return array
*/
public function __debugInfo()
{
return $this->toArray();
}
/**
* Returns a string representation of the collection, which is invoked when
* the collection is converted to a string.
*/
public function __toString()
{
return 'object(' . get_class($this) . ')';
}
}

View File

@@ -0,0 +1,419 @@
<?php
namespace Ds\Traits;
use Ds\Sequence;
use OutOfRangeException;
use UnderflowException;
/**
* Common functionality of all structures that implement 'Sequence'. Because the
* polyfill's only goal is to achieve consistent behaviour, all sequences will
* share the same implementation using an array array.
*
* @package Ds\Traits
*/
trait GenericSequence
{
/**
* @var array internal array used to store the values of the sequence.
*
* @psalm-var array<TValue>
*/
private $array = [];
/**
* @param iterable $values
*
* @psalm-param iterable<TValue> $values
*/
public function __construct(iterable $values = [])
{
foreach ($values as $value) {
$this->push($value);
}
$this->capacity = max(
$values === null ? 0 : count($values),
$this::MIN_CAPACITY
);
}
/**
* @inheritdoc
*/
public function toArray(): array
{
return $this->array;
}
/**
* @inheritdoc
*/
public function apply(callable $callback)
{
foreach ($this->array as &$value) {
$value = $callback($value);
}
}
/**
* @inheritdoc
*/
public function merge($values): Sequence
{
$copy = $this->copy();
$copy->push(...$values);
return $copy;
}
/**
* @inheritdoc
*/
public function count(): int
{
return count($this->array);
}
/**
* @inheritDoc
*/
public function contains(...$values): bool
{
foreach ($values as $value) {
if ($this->find($value) === null) {
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function filter(callable $callback = null): Sequence
{
return new self(array_filter($this->array, $callback ?: 'boolval'));
}
/**
* @inheritDoc
*/
public function find($value)
{
$offset = array_search($value, $this->array, true);
return $offset === false ? null : $offset;
}
/**
* @inheritDoc
*/
public function first()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->array[0];
}
/**
* @inheritDoc
*/
public function get(int $index)
{
if ( ! $this->validIndex($index)) {
throw new OutOfRangeException();
}
return $this->array[$index];
}
/**
* @inheritDoc
*/
public function insert(int $index, ...$values)
{
if ( ! $this->validIndex($index) && $index !== count($this)) {
throw new OutOfRangeException();
}
array_splice($this->array, $index, 0, $values);
$this->checkCapacity();
}
/**
* @inheritDoc
*/
public function join(string $glue = null): string
{
return implode($glue ?? '', $this->array);
}
/**
* @inheritDoc
*/
public function last()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->array[count($this) - 1];
}
/**
* @inheritDoc
*/
public function map(callable $callback): Sequence
{
return new self(array_map($callback, $this->array));
}
/**
* @inheritDoc
*/
public function pop()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
$value = array_pop($this->array);
$this->checkCapacity();
return $value;
}
/**
* @inheritDoc
*/
public function push(...$values)
{
$this->ensureCapacity($this->count() + count($values));
foreach ($values as $value) {
$this->array[] = $value;
}
}
/**
* @inheritDoc
*/
public function reduce(callable $callback, $initial = null)
{
return array_reduce($this->array, $callback, $initial);
}
/**
* @inheritDoc
*/
public function remove(int $index)
{
if ( ! $this->validIndex($index)) {
throw new OutOfRangeException();
}
$value = array_splice($this->array, $index, 1, null)[0];
$this->checkCapacity();
return $value;
}
/**
* @inheritDoc
*/
public function reverse()
{
$this->array = array_reverse($this->array);
}
/**
* @inheritDoc
*/
public function reversed(): Sequence
{
return new self(array_reverse($this->array));
}
/**
* Converts negative or large rotations into the minimum positive number
* of rotations required to rotate the sequence by a given $r.
*/
private function normalizeRotations(int $r)
{
$n = count($this);
if ($n < 2) return 0;
if ($r < 0) return $n - (abs($r) % $n);
return $r % $n;
}
/**
* @inheritDoc
*/
public function rotate(int $rotations)
{
for ($r = $this->normalizeRotations($rotations); $r > 0; $r--) {
array_push($this->array, array_shift($this->array));
}
}
/**
* @inheritDoc
*/
public function set(int $index, $value)
{
if ( ! $this->validIndex($index)) {
throw new OutOfRangeException();
}
$this->array[$index] = $value;
}
/**
* @inheritDoc
*/
public function shift()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
$value = array_shift($this->array);
$this->checkCapacity();
return $value;
}
/**
* @inheritDoc
*/
public function slice(int $offset, int $length = null): Sequence
{
if (func_num_args() === 1) {
$length = count($this);
}
return new self(array_slice($this->array, $offset, $length));
}
/**
* @inheritDoc
*/
public function sort(callable $comparator = null)
{
if ($comparator) {
usort($this->array, $comparator);
} else {
sort($this->array);
}
}
/**
* @inheritDoc
*/
public function sorted(callable $comparator = null): Sequence
{
$copy = $this->copy();
$copy->sort($comparator);
return $copy;
}
/**
* @inheritDoc
*/
public function sum()
{
return array_sum($this->array);
}
/**
* @inheritDoc
*/
public function unshift(...$values)
{
if ($values) {
$this->array = array_merge($values, $this->array);
$this->checkCapacity();
}
}
/**
*
*/
private function validIndex(int $index)
{
return $index >= 0 && $index < count($this);
}
/**
*
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
foreach ($this->array as $value) {
yield $value;
}
}
/**
* @inheritdoc
*/
public function clear()
{
$this->array = [];
$this->capacity = self::MIN_CAPACITY;
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->push($value);
} else {
$this->set($offset, $value);
}
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function &offsetGet($offset)
{
if ( ! $this->validIndex($offset)) {
throw new OutOfRangeException();
}
return $this->array[$offset];
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
if (is_integer($offset) && $this->validIndex($offset)) {
$this->remove($offset);
}
}
/**
* @inheritdoc
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return is_integer($offset)
&& $this->validIndex($offset)
&& $this->get($offset) !== null;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Ds\Traits;
/**
* Common to structures that require a capacity which is a power of two.
*/
trait SquaredCapacity
{
use Capacity;
/**
* Rounds an integer to the next power of two if not already a power of two.
*
* @param int $capacity
*
* @return int
*/
private function square(int $capacity): int
{
return pow(2, ceil(log($capacity, 2)));
}
/**
* Ensures that enough memory is allocated for a specified capacity. This
* potentially reduces the number of reallocations as the size increases.
*
* @param int $capacity The number of values for which capacity should be
* allocated. Capacity will stay the same if this value
* is less than or equal to the current capacity.
*/
public function allocate(int $capacity)
{
$this->capacity = max($this->square($capacity), $this->capacity);
}
/**
* Called when capacity should be increased to accommodate new values.
*/
protected function increaseCapacity()
{
$this->capacity = $this->square(
max(
count($this) + 1,
$this->capacity * $this->getGrowthFactor()
)
);
}
/**
* @param int $total
*/
protected function ensureCapacity(int $total)
{
while ($total > $this->capacity()) {
$this->increaseCapacity();
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Ds;
/**
* A Vector is a sequence of values in a contiguous buffer that grows and
* shrinks automatically. Its the most efficient sequential structure because
* a values index is a direct mapping to its index in the buffer, and the
* growth factor isn't bound to a specific multiple or exponent.
*
* @package Ds
*
* @template TValue
* @implements Sequence<TValue>
*/
final class Vector implements Sequence
{
use Traits\GenericCollection;
use Traits\GenericSequence;
use Traits\Capacity;
public const MIN_CAPACITY = 8;
protected function getGrowthFactor(): float
{
return 1.5;
}
/**
* @return bool whether capacity should be increased.
*/
protected function shouldIncreaseCapacity(): bool
{
return count($this) > $this->capacity;
}
}