Compare commits

..

3 Commits

Author SHA1 Message Date
Lkeme
86d0e89837 - 2025-02-18 18:49:19 +08:00
Lkeme
34345a9870 - 2025-02-18 16:23:58 +08:00
Lkeme
22833cc006 [update] Version 2.4.5 2025-02-18 16:13:22 +08:00
60 changed files with 5626 additions and 33 deletions

View File

@ -29,7 +29,7 @@
<p align=center>
<img src="https://img.shields.io/badge/Version-2.4.3.241231-orange.svg?longCache=true&style=for-the-badge" alt="">
<img src="https://img.shields.io/badge/Version-2.4.5.250218-orange.svg?longCache=true&style=for-the-badge" alt="">
<img src="https://img.shields.io/badge/PHP-8.1+-green.svg?longCache=true&style=for-the-badge" alt="">
<img src="https://img.shields.io/badge/Composer-latest-blueviolet.svg?longCache=true&style=for-the-badge" alt="">
<img src="https://img.shields.io/badge/License-mit-blue.svg?longCache=true&style=for-the-badge" alt="">

View File

@ -22,23 +22,32 @@
"source": "https://github.com/lkeme/BiliHelper-personal"
},
"repositories": [
{
"description": "fire015/flintstone",
"type": "path",
"url": "packages/flintstone",
"canonical": false
},
{
"description": "php-ds/php-ds",
"type": "path",
"url": "packages/php-ds",
"canonical": false
},
{
"description": "PhpComposer",
"type": "composer",
"url": "https://packagist.phpcomposer.com",
"canonical": false
"url": "https://packagist.phpcomposer.com"
},
{
"description": "TencentCloud",
"type": "composer",
"url": "https://mirrors.tencent.com/composer/",
"canonical": false
"url": "https://mirrors.tencent.com/composer/"
},
{
"description": "AliCloud",
"type": "composer",
"url": "https://mirrors.aliyun.com/composer/",
"canonical": false
"url": "https://mirrors.aliyun.com/composer/"
}
],
"require": {
@ -51,17 +60,18 @@
"ext-mbstring": "*",
"monolog/monolog": "dev-main",
"bramus/monolog-colored-line-formatter": "dev-master",
"symfony/yaml": "^6.0",
"symfony/yaml": "^7.0",
"toolkit/stdlib": "*",
"adhocore/cli": "^v1.0.0",
"lkeme/data": "4.x-dev",
"jbzoo/data": "dev-master",
"grasmash/expander": "dev-main",
"amphp/amp": "3.x-dev",
"fire015/flintstone": "dev-master",
"overtrue/pinyin": "dev-master",
"guzzlehttp/guzzle": "^7.0",
"toolkit/pflag": "dev-main",
"symfony/console": "^6.0",
"symfony/console": "^7.0",
"fire015/flintstone": "dev-master",
"php-ds/php-ds": "dev-master as v1.1",
"malios/php-to-ascii-table": "dev-master"
},
"autoload": {

View File

@ -8,6 +8,25 @@
[comment]: <> (</details>)
## v2.4.5.250218 alpha (2025-02-18)
### Added
-
### Changed
- 更新设备信息
### Fixed
- 尝试修复(-403)账号异常,操作失败
- Make database config type explicitely nullable (fix php84 deprecation)
### Remarks
- Goodbye 2024
## v2.4.3.241231 alpha (2024-12-31)
### Added

View File

@ -0,0 +1 @@
github: fire015

4
packages/flintstone/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor
composer.lock
.idea
.phpunit.result.cache

View File

@ -0,0 +1,14 @@
language: php
sudo: false
dist: bionic
php:
- 7.3
- 7.4
- 8.0
- 8.1.0
install:
- travis_retry composer install --no-interaction --prefer-dist
script: vendor/bin/phpunit

View File

@ -0,0 +1,67 @@
Change Log
==========
### 19/01/2021 - 2.3
* Bump minimum PHP version to 7.3
* Update PHPUnit to version 9 (ensure Flintstone is compatible with PHP 8)
### 12/03/2019 - 2.2
* Bump minimum PHP version to 7.0
* Update PHPUnit to version 6
* Removed data type validation for storing
* Added param and return types
### 09/06/2017 - 2.1.1
* Update `Database::writeTempToFile` to correctly close the file pointer and free up memory
### 24/05/2017 - 2.1
* Bump minimum PHP version to 5.6
* Tidy up of Flintstone class, moved some code into `Database`
* Added `Line` and `Validation` classes
* Closed off public methods `Database::openFile` and `Database::closeFile`
### 20/01/2016 - 2.0
* Major refactor, class names have changed and the whole codebase is much more extensible
* Removed the static `load` and `unload` methods and the `FlinstoneDB` class
* The `replace` method is no longer public
* The `getFile` method has been removed
* Default swap memory limit has been increased to 2MB
* Ability to pass any instance for cache that implements `Flintstone\Cache\CacheInterface`
### 25/03/2015 - 1.9
* Added `getAll` method and some refactoring
### 15/10/2014 - 1.8
* Added formatter option so that you can control how data is encoded/decoded (default is serialize but also ships with json)
### 09/10/2014 - 1.7
* Moved from fopen to SplFileObject
* Moved composer loader from PSR-0 to PSR-4
* Code is now PSR-2 compliant
* Added PHP 5.6 to travis
### 30/09/2014 - 1.6
* Updated limits on valid characters in key name and size
* Improved unit tests
### 29/05/2014 - 1.5
* Reduced some internal complexity
* Fixed gzip compression
* Unit tests now running against all options
* Removed `setOptions` method, must be passed into the `load` method
### 11/03/2014 - 1.4
* Now using Composer
### 16/07/2013 - 1.3
* Changed the load method to static so that multiple instances can be loaded without conflict (use Flintstone::load now instead of $db->load)
* Exception thrown is now FlintstoneException
### 23/01/2013 - 1.2
* Removed the multibyte unserialize method as it seems to work without
### 22/06/2012 - 1.1
* Added new method getKeys() to return an array of keys in the database
### 17/06/2011 - 1.0
* Initial release

View File

@ -0,0 +1,18 @@
# MIT License
Copyright (c) 2010-2017 Jason M
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,105 @@
Flintstone
==========
[![Total Downloads](https://img.shields.io/packagist/dm/fire015/flintstone.svg)](https://packagist.org/packages/fire015/flintstone)
[![Build Status](https://travis-ci.com/fire015/flintstone.svg?branch=master)](https://travis-ci.com/github/fire015/flintstone)
A key/value database store using flat files for PHP.
Features include:
* Memory efficient
* File locking
* Caching
* Gzip compression
* Easy to use
### Installation
The easiest way to install Flintstone is via [composer](http://getcomposer.org/). Run the following command to install it.
```
composer require fire015/flintstone
```
```php
<?php
require 'vendor/autoload.php';
use Flintstone\Flintstone;
$users = new Flintstone('users', ['dir' => '/path/to/database/dir/']);
```
### Requirements
- PHP 7.3+
### Data types
Flintstone can store any data type that can be formatted into a string. By default this uses `serialize()`. See [Changing the formatter](#changing-the-formatter) for more details.
### Options
|Name |Type |Default Value |Description |
|--- |--- |--- |--- |
|dir |string |the current working directory |The directory where the database files are stored (this should be somewhere that is not web accessible) e.g. /path/to/database/ |
|ext |string |.dat |The database file extension to use |
|gzip |boolean |false |Use gzip to compress the database |
|cache |boolean or object |true |Whether to cache `get()` results for faster data retrieval |
|formatter |null or object |null |The formatter class used to encode/decode data |
|swap_memory_limit |integer |2097152 |The amount of memory to use before writing to a temporary file |
### Usage examples
```php
<?php
// Load a database
$users = new Flintstone('users', ['dir' => '/path/to/database/dir/']);
// Set a key
$users->set('bob', ['email' => 'bob@site.com', 'password' => '123456']);
// Get a key
$user = $users->get('bob');
echo 'Bob, your email is ' . $user['email'];
// Retrieve all key names
$keys = $users->getKeys(); // returns array('bob')
// Retrieve all data
$data = $users->getAll(); // returns array('bob' => array('email' => 'bob@site.com', 'password' => '123456'));
// Delete a key
$users->delete('bob');
// Flush the database
$users->flush();
```
### Changing the formatter
By default Flintstone will encode/decode data using PHP's serialize functions, however you can override this with your own class if you prefer.
Just make sure it implements `Flintstone\Formatter\FormatterInterface` and then you can provide it as the `formatter` option.
If you wish to use JSON as the formatter, Flintstone already ships with this as per the example below:
```php
<?php
require 'vendor/autoload.php';
use Flintstone\Flintstone;
use Flintstone\Formatter\JsonFormatter;
$users = new Flintstone('users', [
'dir' => __DIR__,
'formatter' => new JsonFormatter()
]);
```
### Changing the cache
To speed up data retrieval Flintstone can store the results of `get()` in a cache store. By default this uses a simple array that only persist's for as long as the `Flintstone` object exists.
If you want to use your own cache store (such as Memcached) you can pass a class as the `cache` option. Just make sure it implements `Flintstone\Cache\CacheInterface`.

View File

@ -0,0 +1,42 @@
Upgrading from version 1.x to 2.x
=================================
As Flintstone is no longer loaded statically the major change required is to switch from using the static `load` method to just instantiating a new instance of Flinstone.
The `FlinstoneDB` class has also been removed and `Flintstone\FlintstoneException` is now `Flintstone\Exception`.
### Version 1.x:
```php
<?php
require 'vendor/autoload.php';
use Flintstone\Flintstone;
use Flintstone\FlintstoneException;
try {
$users = Flintstone::load('users', array('dir' => '/path/to/database/dir/'));
}
catch (FlintstoneException $e) {
}
```
### Version 2.x:
```php
<?php
require 'vendor/autoload.php';
use Flintstone\Flintstone;
use Flintstone\Exception;
try {
$users = new Flintstone('users', array('dir' => '/path/to/database/dir/'));
}
catch (Exception $e) {
}
```
See CHANGELOG.md for further changes.

View File

@ -0,0 +1,25 @@
{
"name": "fire015/flintstone",
"type": "library",
"description": "A key/value database store using flat files for PHP",
"keywords": ["flintstone", "database", "cache", "files", "memory"],
"homepage": "https://github.com/fire015/flintstone",
"license": "MIT",
"authors": [
{
"name": "Jason M",
"email": "emailfire@gmail.com"
}
],
"require": {
"php": ">=7.3"
},
"autoload": {
"psr-4": {
"Flintstone\\": "src/"
}
},
"require-dev" : {
"phpunit/phpunit": "^9"
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" verbose="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Flintstone Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,53 @@
<?php
namespace Flintstone\Cache;
class ArrayCache implements CacheInterface
{
/**
* Cache data.
*
* @var array
*/
protected $cache = [];
/**
* {@inheritdoc}
*/
public function contains($key)
{
return array_key_exists($key, $this->cache);
}
/**
* {@inheritdoc}
*/
public function get($key)
{
return $this->cache[$key];
}
/**
* {@inheritdoc}
*/
public function set($key, $data)
{
$this->cache[$key] = $data;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
unset($this->cache[$key]);
}
/**
* {@inheritdoc}
*/
public function flush()
{
$this->cache = [];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Flintstone\Cache;
interface CacheInterface
{
/**
* Check if a key exists in the cache.
*
* @param string $key
*
* @return bool
*/
public function contains($key);
/**
* Get a key from the cache.
*
* @param string $key
*
* @return mixed
*/
public function get($key);
/**
* Set a key in the cache.
*
* @param string $key
* @param mixed $data
*/
public function set($key, $data);
/**
* Delete a key from the cache.
*
* @param string $key
*/
public function delete($key);
/**
* Flush the cache.
*/
public function flush();
}

View File

@ -0,0 +1,209 @@
<?php
namespace Flintstone;
use Flintstone\Cache\ArrayCache;
use Flintstone\Cache\CacheInterface;
use Flintstone\Formatter\FormatterInterface;
use Flintstone\Formatter\SerializeFormatter;
class Config
{
/**
* Config.
*
* @var array
*/
protected $config = [];
/**
* Constructor.
*
* @param array $config
*/
public function __construct(array $config = [])
{
$config = $this->normalizeConfig($config);
$this->setDir($config['dir']);
$this->setExt($config['ext']);
$this->setGzip($config['gzip']);
$this->setCache($config['cache']);
$this->setFormatter($config['formatter']);
$this->setSwapMemoryLimit($config['swap_memory_limit']);
}
/**
* Normalize the user supplied config.
*
* @param array $config
*
* @return array
*/
protected function normalizeConfig(array $config): array
{
$defaultConfig = [
'dir' => getcwd(),
'ext' => '.dat',
'gzip' => false,
'cache' => true,
'formatter' => null,
'swap_memory_limit' => 2097152, // 2MB
];
return array_replace($defaultConfig, $config);
}
/**
* Get the dir.
*
* @return string
*/
public function getDir(): string
{
return $this->config['dir'];
}
/**
* Set the dir.
*
* @param string $dir
*
* @throws Exception
*/
public function setDir(string $dir)
{
if (!is_dir($dir)) {
throw new Exception('Directory does not exist: ' . $dir);
}
$this->config['dir'] = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
}
/**
* Get the ext.
*
* @return string
*/
public function getExt(): string
{
if ($this->useGzip()) {
return $this->config['ext'] . '.gz';
}
return $this->config['ext'];
}
/**
* Set the ext.
*
* @param string $ext
*/
public function setExt(string $ext)
{
if (substr($ext, 0, 1) !== '.') {
$ext = '.' . $ext;
}
$this->config['ext'] = $ext;
}
/**
* Use gzip?
*
* @return bool
*/
public function useGzip(): bool
{
return $this->config['gzip'];
}
/**
* Set gzip.
*
* @param bool $gzip
*/
public function setGzip(bool $gzip)
{
$this->config['gzip'] = $gzip;
}
/**
* Get the cache.
*
* @return CacheInterface|false
*/
public function getCache()
{
return $this->config['cache'];
}
/**
* Set the cache.
*
* @param mixed $cache
*
* @throws Exception
*/
public function setCache($cache)
{
if (!is_bool($cache) && !$cache instanceof CacheInterface) {
throw new Exception('Cache must be a boolean or an instance of Flintstone\Cache\CacheInterface');
}
if ($cache === true) {
$cache = new ArrayCache();
}
$this->config['cache'] = $cache;
}
/**
* Get the formatter.
*
* @return FormatterInterface
*/
public function getFormatter(): FormatterInterface
{
return $this->config['formatter'];
}
/**
* Set the formatter.
*
* @param FormatterInterface|null $formatter
*
* @throws Exception
*/
public function setFormatter($formatter)
{
if ($formatter === null) {
$formatter = new SerializeFormatter();
}
if (!$formatter instanceof FormatterInterface) {
throw new Exception('Formatter must be an instance of Flintstone\Formatter\FormatterInterface');
}
$this->config['formatter'] = $formatter;
}
/**
* Get the swap memory limit.
*
* @return int
*/
public function getSwapMemoryLimit(): int
{
return $this->config['swap_memory_limit'];
}
/**
* Set the swap memory limit.
*
* @param int $limit
*/
public function setSwapMemoryLimit(int $limit)
{
$this->config['swap_memory_limit'] = $limit;
}
}

View File

@ -0,0 +1,255 @@
<?php
namespace Flintstone;
use SplFileObject;
use SplTempFileObject;
class Database
{
/**
* File read flag.
*
* @var int
*/
const FILE_READ = 1;
/**
* File write flag.
*
* @var int
*/
const FILE_WRITE = 2;
/**
* File append flag.
*
* @var int
*/
const FILE_APPEND = 3;
/**
* File access mode.
*
* @var array
*/
protected $fileAccessMode = [
self::FILE_READ => [
'mode' => 'rb',
'operation' => LOCK_SH,
],
self::FILE_WRITE => [
'mode' => 'wb',
'operation' => LOCK_EX,
],
self::FILE_APPEND => [
'mode' => 'ab',
'operation' => LOCK_EX,
],
];
/**
* Database name.
*
* @var string
*/
protected $name;
/**
* Config class.
*
* @var Config
*/
protected $config;
/**
* Constructor.
*
* @param string $name
* @param Config|null $config
*/
public function __construct(string $name, Config|null $config = null)
{
$this->setName($name);
if ($config) {
$this->setConfig($config);
}
}
/**
* Get the database name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Set the database name.
*
* @param string $name
*
* @throws Exception
*/
public function setName(string $name)
{
Validation::validateDatabaseName($name);
$this->name = $name;
}
/**
* Get the config.
*
* @return Config
*/
public function getConfig(): Config
{
return $this->config;
}
/**
* Set the config.
*
* @param Config $config
*/
public function setConfig(Config $config)
{
$this->config = $config;
}
/**
* Get the path to the database file.
*
* @return string
*/
public function getPath(): string
{
return $this->config->getDir() . $this->getName() . $this->config->getExt();
}
/**
* Open the database file.
*
* @param int $mode
*
* @throws Exception
*
* @return SplFileObject
*/
protected function openFile(int $mode): SplFileObject
{
$path = $this->getPath();
if (!is_file($path) && !@touch($path)) {
throw new Exception('Could not create file: ' . $path);
}
if (!is_readable($path) || !is_writable($path)) {
throw new Exception('File does not have permission for read and write: ' . $path);
}
if ($this->getConfig()->useGzip()) {
$path = 'compress.zlib://' . $path;
}
$res = $this->fileAccessMode[$mode];
$file = new SplFileObject($path, $res['mode']);
if ($mode === self::FILE_READ) {
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
}
if (!$this->getConfig()->useGzip() && !$file->flock($res['operation'])) {
$file = null;
throw new Exception('Could not lock file: ' . $path);
}
return $file;
}
/**
* Open a temporary file.
*
* @return SplTempFileObject
*/
public function openTempFile(): SplTempFileObject
{
return new SplTempFileObject($this->getConfig()->getSwapMemoryLimit());
}
/**
* Close the database file.
*
* @param SplFileObject $file
*
* @throws Exception
*/
protected function closeFile(SplFileObject &$file)
{
if (!$this->getConfig()->useGzip() && !$file->flock(LOCK_UN)) {
$file = null;
throw new Exception('Could not unlock file');
}
$file = null;
}
/**
* Read lines from the database file.
*
* @return \Generator
*/
public function readFromFile(): \Generator
{
$file = $this->openFile(static::FILE_READ);
try {
foreach ($file as $line) {
yield new Line($line);
}
} finally {
$this->closeFile($file);
}
}
/**
* Append a line to the database file.
*
* @param string $line
*/
public function appendToFile(string $line)
{
$file = $this->openFile(static::FILE_APPEND);
$file->fwrite($line);
$this->closeFile($file);
}
/**
* Flush the database file.
*/
public function flushFile()
{
$file = $this->openFile(static::FILE_WRITE);
$this->closeFile($file);
}
/**
* Write temporary file contents to database file.
*
* @param SplTempFileObject $tmpFile
*/
public function writeTempToFile(SplTempFileObject &$tmpFile)
{
$file = $this->openFile(static::FILE_WRITE);
foreach ($tmpFile as $line) {
$file->fwrite($line);
}
$this->closeFile($file);
$tmpFile = null;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Flintstone;
class Exception extends \Exception
{
}

View File

@ -0,0 +1,285 @@
<?php
namespace Flintstone;
class Flintstone
{
/**
* Flintstone version.
*
* @var string
*/
const VERSION = '2.3';
/**
* Database class.
*
* @var Database
*/
protected $database;
/**
* Config class.
*
* @var Config
*/
protected $config;
/**
* Constructor.
*
* @param Database|string $database
* @param Config|array $config
*/
public function __construct($database, $config)
{
if (is_string($database)) {
$database = new Database($database);
}
if (is_array($config)) {
$config = new Config($config);
}
$this->setDatabase($database);
$this->setConfig($config);
}
/**
* Get the database.
*
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Set the database.
*
* @param Database $database
*/
public function setDatabase(Database $database)
{
$this->database = $database;
}
/**
* Get the config.
*
* @return Config
*/
public function getConfig(): Config
{
return $this->config;
}
/**
* Set the config.
*
* @param Config $config
*/
public function setConfig(Config $config)
{
$this->config = $config;
$this->getDatabase()->setConfig($config);
}
/**
* Get a key from the database.
*
* @param string $key
*
* @return mixed
*/
public function get(string $key)
{
Validation::validateKey($key);
// Fetch the key from cache
if ($cache = $this->getConfig()->getCache()) {
if ($cache->contains($key)) {
return $cache->get($key);
}
}
// Fetch the key from database
$file = $this->getDatabase()->readFromFile();
$data = false;
foreach ($file as $line) {
/** @var Line $line */
if ($line->getKey() == $key) {
$data = $this->decodeData($line->getData());
break;
}
}
// Save the data to cache
if ($cache && $data !== false) {
$cache->set($key, $data);
}
return $data;
}
/**
* Set a key in the database.
*
* @param string $key
* @param mixed $data
*/
public function set(string $key, $data)
{
Validation::validateKey($key);
// If the key already exists we need to replace it
if ($this->get($key) !== false) {
$this->replace($key, $data);
return;
}
// Write the key to the database
$this->getDatabase()->appendToFile($this->getLineString($key, $data));
// Delete the key from cache
if ($cache = $this->getConfig()->getCache()) {
$cache->delete($key);
}
}
/**
* Delete a key from the database.
*
* @param string $key
*/
public function delete(string $key)
{
Validation::validateKey($key);
if ($this->get($key) !== false) {
$this->replace($key, false);
}
}
/**
* Flush the database.
*/
public function flush()
{
$this->getDatabase()->flushFile();
// Flush the cache
if ($cache = $this->getConfig()->getCache()) {
$cache->flush();
}
}
/**
* Get all keys from the database.
*
* @return array
*/
public function getKeys(): array
{
$keys = [];
$file = $this->getDatabase()->readFromFile();
foreach ($file as $line) {
/** @var Line $line */
$keys[] = $line->getKey();
}
return $keys;
}
/**
* Get all data from the database.
*
* @return array
*/
public function getAll(): array
{
$data = [];
$file = $this->getDatabase()->readFromFile();
foreach ($file as $line) {
/** @var Line $line */
$data[$line->getKey()] = $this->decodeData($line->getData());
}
return $data;
}
/**
* Replace a key in the database.
*
* @param string $key
* @param mixed $data
*/
protected function replace(string $key, $data)
{
// Write a new database to a temporary file
$tmpFile = $this->getDatabase()->openTempFile();
$file = $this->getDatabase()->readFromFile();
foreach ($file as $line) {
/** @var Line $line */
if ($line->getKey() == $key) {
if ($data !== false) {
$tmpFile->fwrite($this->getLineString($key, $data));
}
} else {
$tmpFile->fwrite($line->getLine() . "\n");
}
}
$tmpFile->rewind();
// Overwrite the database with the temporary file
$this->getDatabase()->writeTempToFile($tmpFile);
// Delete the key from cache
if ($cache = $this->getConfig()->getCache()) {
$cache->delete($key);
}
}
/**
* Get the line string to write.
*
* @param string $key
* @param mixed $data
*
* @return string
*/
protected function getLineString(string $key, $data): string
{
return $key . '=' . $this->encodeData($data) . "\n";
}
/**
* Decode a string into data.
*
* @param string $data
*
* @return mixed
*/
protected function decodeData(string $data)
{
return $this->getConfig()->getFormatter()->decode($data);
}
/**
* Encode data into a string.
*
* @param mixed $data
*
* @return string
*/
protected function encodeData($data): string
{
return $this->getConfig()->getFormatter()->encode($data);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Flintstone\Formatter;
interface FormatterInterface
{
/**
* Encode data into a string.
*
* @param mixed $data
*
* @return string
*/
public function encode($data): string;
/**
* Decode a string into data.
*
* @param string $data
*
* @return mixed
*/
public function decode(string $data);
}

View File

@ -0,0 +1,46 @@
<?php
namespace Flintstone\Formatter;
use Flintstone\Exception;
class JsonFormatter implements FormatterInterface
{
/**
* @var bool
*/
private $assoc;
public function __construct(bool $assoc = true)
{
$this->assoc = $assoc;
}
/**
* {@inheritdoc}
*/
public function encode($data): string
{
$result = json_encode($data);
if (json_last_error() === JSON_ERROR_NONE) {
return $result;
}
throw new Exception(json_last_error_msg());
}
/**
* {@inheritdoc}
*/
public function decode(string $data)
{
$result = json_decode($data, $this->assoc);
if (json_last_error() === JSON_ERROR_NONE) {
return $result;
}
throw new Exception(json_last_error_msg());
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Flintstone\Formatter;
class SerializeFormatter implements FormatterInterface
{
/**
* {@inheritdoc}
*/
public function encode($data): string
{
return serialize($this->preserveLines($data, false));
}
/**
* {@inheritdoc}
*/
public function decode(string $data)
{
return $this->preserveLines(unserialize($data), true);
}
/**
* Preserve new lines, recursive function.
*
* @param mixed $data
* @param bool $reverse
*
* @return mixed
*/
protected function preserveLines($data, bool $reverse)
{
$search = ["\n", "\r"];
$replace = ['\\n', '\\r'];
if ($reverse) {
$search = ['\\n', '\\r'];
$replace = ["\n", "\r"];
}
if (is_string($data)) {
$data = str_replace($search, $replace, $data);
} elseif (is_array($data)) {
foreach ($data as &$value) {
$value = $this->preserveLines($value, $reverse);
}
unset($value);
}
return $data;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Flintstone;
class Line
{
/**
* @var string
*/
protected $line;
/**
* @var array
*/
protected $pieces = [];
public function __construct(string $line)
{
$this->line = $line;
$this->pieces = explode('=', $line, 2);
}
public function getLine(): string
{
return $this->line;
}
public function getKey(): string
{
return $this->pieces[0];
}
public function getData(): string
{
return $this->pieces[1];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Flintstone;
class Validation
{
/**
* Validate the key.
*
* @param string $key
*
* @throws Exception
*/
public static function validateKey(string $key)
{
if (empty($key) || !preg_match('/^[\w-]+$/', $key)) {
throw new Exception('Invalid characters in key');
}
}
/**
* Check the database name is valid.
*
* @param string $name
*
* @throws Exception
*/
public static function validateDatabaseName(string $name)
{
if (empty($name) || !preg_match('/^[\w-]+$/', $name)) {
throw new Exception('Invalid characters in database name');
}
}
}

View File

@ -0,0 +1,46 @@
<?php
use Flintstone\Cache\ArrayCache;
class ArrayCacheTest extends \PHPUnit\Framework\TestCase
{
/**
* @var ArrayCache
*/
private $cache;
protected function setUp(): void
{
$this->cache = new ArrayCache();
}
/**
* @test
*/
public function canGetAndSet()
{
$this->cache->set('foo', 'bar');
$this->assertTrue($this->cache->contains('foo'));
$this->assertEquals('bar', $this->cache->get('foo'));
}
/**
* @test
*/
public function canDelete()
{
$this->cache->set('foo', 'bar');
$this->cache->delete('foo');
$this->assertFalse($this->cache->contains('foo'));
}
/**
* @test
*/
public function canFlush()
{
$this->cache->set('foo', 'bar');
$this->cache->flush();
$this->assertFalse($this->cache->contains('foo'));
}
}

View File

@ -0,0 +1,85 @@
<?php
use Flintstone\Cache\ArrayCache;
use Flintstone\Config;
use Flintstone\Formatter\JsonFormatter;
use Flintstone\Formatter\SerializeFormatter;
class ConfigTest extends \PHPUnit\Framework\TestCase
{
/**
* @test
*/
public function defaultConfigIsSet()
{
$config = new Config();
$this->assertEquals(getcwd().DIRECTORY_SEPARATOR, $config->getDir());
$this->assertEquals('.dat', $config->getExt());
$this->assertFalse($config->useGzip());
$this->assertInstanceOf(ArrayCache::class, $config->getCache());
$this->assertInstanceOf(SerializeFormatter::class, $config->getFormatter());
$this->assertEquals(2097152, $config->getSwapMemoryLimit());
}
/**
* @test
*/
public function constructorConfigOverride()
{
$config = new Config([
'dir' => __DIR__,
'ext' => 'test',
'gzip' => true,
'cache' => false,
'formatter' => null,
'swap_memory_limit' => 100,
]);
$this->assertEquals(__DIR__.DIRECTORY_SEPARATOR, $config->getDir());
$this->assertEquals('.test.gz', $config->getExt());
$this->assertTrue($config->useGzip());
$this->assertFalse($config->getCache());
$this->assertInstanceOf(SerializeFormatter::class, $config->getFormatter());
$this->assertEquals(100, $config->getSwapMemoryLimit());
}
/**
* @test
*/
public function setValidFormatter()
{
$config = new Config();
$config->setFormatter(new JsonFormatter());
$this->assertInstanceOf(JsonFormatter::class, $config->getFormatter());
}
/**
* @test
*/
public function setInvalidFormatter()
{
$this->expectException(\Flintstone\Exception::class);
$config = new Config();
$config->setFormatter(new self());
}
/**
* @test
*/
public function invalidDirSet()
{
$this->expectException(\Flintstone\Exception::class);
$config = new Config();
$config->setDir('/x/y/z/foo');
}
/**
* @test
*/
public function invalidCacheSet()
{
$this->expectException(\Flintstone\Exception::class);
$config = new Config();
$config->setCache(new self());
}
}

View File

@ -0,0 +1,96 @@
<?php
use Flintstone\Config;
use Flintstone\Database;
use Flintstone\Line;
class DatabaseTest extends \PHPUnit\Framework\TestCase
{
/**
* @var Database
*/
private $db;
protected function setUp(): void
{
$config = new Config([
'dir' => __DIR__,
]);
$this->db = new Database('test', $config);
}
protected function tearDown(): void
{
if (is_file($this->db->getPath())) {
unlink($this->db->getPath());
}
}
/**
* @test
*/
public function databaseHasInvalidName()
{
$this->expectException(\Flintstone\Exception::class);
$config = new Config();
new Database('test!123', $config);
}
/**
* @test
*/
public function canGetDatabaseAndConfig()
{
$this->assertEquals('test', $this->db->getName());
$this->assertInstanceOf(Config::class, $this->db->getConfig());
$this->assertEquals(__DIR__ . DIRECTORY_SEPARATOR . 'test.dat', $this->db->getPath());
}
/**
* @test
*/
public function canAppendToFile()
{
$this->db->appendToFile('foo=bar');
$this->assertEquals('foo=bar', file_get_contents($this->db->getPath()));
}
/**
* @test
*/
public function canFlushFile()
{
$this->db->appendToFile('foo=bar');
$this->db->flushFile();
$this->assertEmpty(file_get_contents($this->db->getPath()));
}
/**
* @test
*/
public function canReadFromFile()
{
$this->db->appendToFile('foo=bar');
$file = $this->db->readFromFile();
foreach ($file as $line) {
$this->assertInstanceOf(Line::class, $line);
$this->assertEquals('foo', $line->getKey());
$this->assertEquals('bar', $line->getData());
}
}
/**
* @test
*/
public function canWriteTempToFile()
{
$tmpFile = new SplTempFileObject();
$tmpFile->fwrite('foo=bar');
$tmpFile->rewind();
$this->db->writeTempToFile($tmpFile);
$this->assertEquals('foo=bar', file_get_contents($this->db->getPath()));
}
}

View File

@ -0,0 +1,97 @@
<?php
use Flintstone\Config;
use Flintstone\Database;
use Flintstone\Flintstone;
use Flintstone\Formatter\JsonFormatter;
class FlintstoneTest extends \PHPUnit\Framework\TestCase
{
public function testGetDatabaseAndConfig()
{
$db = new Flintstone('test', [
'dir' => __DIR__,
'cache' => false,
]);
$this->assertInstanceOf(Database::class, $db->getDatabase());
$this->assertInstanceOf(Config::class, $db->getConfig());
}
/**
* @test
*/
public function keyHasInvalidName()
{
$this->expectException(\Flintstone\Exception::class);
$db = new Flintstone('test', []);
$db->get('test!123');
}
/**
* @test
*/
public function canRunAllOperations()
{
$this->runOperationsTests([
'dir' => __DIR__,
'cache' => false,
'gzip' => false,
]);
$this->runOperationsTests([
'dir' => __DIR__,
'cache' => true,
'gzip' => true,
]);
$this->runOperationsTests([
'dir' => __DIR__,
'cache' => false,
'gzip' => false,
'formatter' => new JsonFormatter(),
]);
}
private function runOperationsTests(array $config)
{
$db = new Flintstone('test', $config);
$arr = ['foo' => "new\nline"];
$this->assertFalse($db->get('foo'));
$db->set('foo', 1);
$db->set('name', 'john');
$db->set('arr', $arr);
$this->assertEquals(1, $db->get('foo'));
$this->assertEquals('john', $db->get('name'));
$this->assertEquals($arr, $db->get('arr'));
$db->set('foo', 2);
$this->assertEquals(2, $db->get('foo'));
$this->assertEquals('john', $db->get('name'));
$this->assertEquals($arr, $db->get('arr'));
$db->delete('name');
$this->assertFalse($db->get('name'));
$this->assertEquals($arr, $db->get('arr'));
$keys = $db->getKeys();
$this->assertEquals(2, count($keys));
$this->assertEquals('foo', $keys[0]);
$this->assertEquals('arr', $keys[1]);
$data = $db->getAll();
$this->assertEquals(2, count($data));
$this->assertEquals(2, $data['foo']);
$this->assertEquals($arr, $data['arr']);
$db->flush();
$this->assertFalse($db->get('foo'));
$this->assertFalse($db->get('arr'));
$this->assertEquals(0, count($db->getKeys()));
$this->assertEquals(0, count($db->getAll()));
unlink($db->getDatabase()->getPath());
}
}

View File

@ -0,0 +1,73 @@
<?php
use Flintstone\Formatter\JsonFormatter;
class JsonFormatterTest extends \PHPUnit\Framework\TestCase
{
/**
* @var JsonFormatter
*/
private $formatter;
protected function setUp(): void
{
$this->formatter = new JsonFormatter();
}
/**
* @test
* @dataProvider validData
*/
public function encodesValidData($originalValue, $encodedValue)
{
$this->assertSame($encodedValue, $this->formatter->encode($originalValue));
}
/**
* @test
* @dataProvider validData
*/
public function decodesValidData($originalValue, $encodedValue)
{
$this->assertSame($originalValue, $this->formatter->decode($encodedValue));
}
/**
* @test
*/
public function decodesAnObject()
{
$originalValue = (object)['foo' => 'bar'];
$formatter = new JsonFormatter(false);
$encodedValue = $formatter->encode($originalValue);
$this->assertEquals($originalValue, $formatter->decode($encodedValue));
}
/**
* @test
*/
public function encodingInvalidDataThrowsException()
{
$this->expectException(\Flintstone\Exception::class);
$this->formatter->encode(chr(241));
}
/**
* @test
*/
public function decodingInvalidDataThrowsException()
{
$this->expectException(\Flintstone\Exception::class);
$this->formatter->decode('{');
}
public function validData(): array
{
return [
[null, 'null'],
[1, '1'],
['foo', '"foo"'],
[["test", "new\nline"], '["test","new\nline"]'],
];
}
}

View File

@ -0,0 +1,44 @@
<?php
use Flintstone\Formatter\SerializeFormatter;
class SerializeFormatterTest extends \PHPUnit\Framework\TestCase
{
/**
* @var SerializeFormatter
*/
private $formatter;
protected function setUp(): void
{
$this->formatter = new SerializeFormatter();
}
/**
* @test
* @dataProvider validData
*/
public function encodesValidData($originalValue, $encodedValue)
{
$this->assertSame($encodedValue, $this->formatter->encode($originalValue));
}
/**
* @test
* @dataProvider validData
*/
public function decodesValidData($originalValue, $encodedValue)
{
$this->assertSame($originalValue, $this->formatter->decode($encodedValue));
}
public function validData(): array
{
return [
[null, 'N;'],
[1, 'i:1;'],
['foo', 's:3:"foo";'],
[["test", "new\nline"], 'a:2:{i:0;s:4:"test";i:1;s:9:"new\nline";}'],
];
}
}

View File

@ -0,0 +1,50 @@
<?php
use Flintstone\Line;
class LineTest extends \PHPUnit\Framework\TestCase
{
/**
* @var Line
*/
private $line;
protected function setUp(): void
{
$this->line = new Line('foo=bar');
}
/**
* @test
*/
public function canGetLine()
{
$this->assertEquals('foo=bar', $this->line->getLine());
}
/**
* @test
*/
public function canGetKey()
{
$this->assertEquals('foo', $this->line->getKey());
}
/**
* @test
*/
public function canGetData()
{
$this->assertEquals('bar', $this->line->getData());
}
/**
* @test
*/
public function canGetKeyAndDataWithMultipleEquals()
{
$line = new Line('foo=bar=baz');
$this->assertEquals('foo', $line->getKey());
$this->assertEquals('bar=baz', $line->getData());
}
}

View File

@ -0,0 +1,24 @@
<?php
use Flintstone\Validation;
class ValidationTest extends \PHPUnit\Framework\TestCase
{
/**
* @test
*/
public function validateKey()
{
$this->expectException(\Flintstone\Exception::class);
Validation::validateKey('test!123');
}
/**
* @test
*/
public function validateDatabaseName()
{
$this->expectException(\Flintstone\Exception::class);
Validation::validateDatabaseName('test!123');
}
}

4
packages/php-ds/.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/phpunit.xml export-ignore

View File

@ -0,0 +1,67 @@
name: "CI"
on:
pull_request:
push:
branches:
- "master"
workflow_dispatch:
jobs:
phpunit:
name: "PHPUnit"
runs-on: "ubuntu-20.04"
strategy:
matrix:
php-version:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
dependencies:
- "highest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
with:
fetch-depth: 2
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
coverage: "pcov"
ini-values: "zend.assertions=1"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
with:
dependency-versions: "${{ matrix.dependencies }}"
- name: "Run PHPUnit"
run: "vendor/bin/phpunit"
upload_coverage:
name: "Upload coverage to Codecov"
runs-on: "ubuntu-20.04"
needs:
- "phpunit"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
with:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v2"
with:
path: "reports"
- name: "Upload to Codecov"
uses: "codecov/codecov-action@v1"
with:
directory: reports

4
packages/php-ds/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.phpunit.result.cache
build
vendor
composer.lock

View File

@ -0,0 +1,41 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
### Fixed
- `Collection` PHPDoc now correctly states that it extends `IteratorAggregate`, rather than just `Traversable`.
## [1.4.1] - 2022-03-09
## [1.4.0] - 2021-11-17
## [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
packages/php-ds/LICENSE 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.

38
packages/php-ds/README.md Normal file
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,33 @@
{
"name": "php-ds/php-ds",
"license": "MIT",
"description": "Specialized data structures as alternatives to the PHP array",
"keywords": ["php", "ds", "data structures", "polyfill"],
"authors": [
{
"name": "Rudi Theunissen",
"email": "rudolf.theunissen@gmail.com"
}
],
"require": {
"php": ">=7.4",
"ext-json": "*"
},
"require-dev": {
"php-ds/tests": "^1.5"
},
"provide": {
"ext-ds": "1.5.0"
},
"suggest": {
"ext-ds": "to improve performance and reduce memory usage"
},
"scripts": {
"test": "phpunit"
},
"autoload": {
"psr-4" : {
"Ds\\": "src"
}
}
}

View File

@ -0,0 +1,25 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
bootstrap="tests/bootstrap.php"
colors="true"
>
<testsuites>
<testsuite name="DS Tests">
<directory>vendor/php-ds/tests/tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
<report>
<clover outputFile="build/logs/coverage.xml" />
<html outputDirectory="build/logs/coverage" />
<text outputFile="build/logs/coverage.txt" />
</report>
</coverage>
</phpunit>

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 \IteratorAggregate<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,31 @@
<?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>
* @template-use Traits\GenericCollection<int, TValue>
* @template-use Traits\GenericSequence<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;
}

812
packages/php-ds/src/Map.php Normal file
View File

@ -0,0 +1,812 @@
<?php
namespace Ds;
use OutOfBoundsException;
use OutOfRangeException;
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>
* @implements \ArrayAccess<TKey, TValue>
* @template-use Traits\GenericCollection<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|null $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|null $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|null $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|null $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|null $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|null $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,197 @@
<?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>
* @implements \ArrayAccess<int, TValue>
* @template-use Traits\GenericCollection<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,331 @@
<?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>
* @extends \ArrayAccess<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|null $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);
}

541
packages/php-ds/src/Set.php Normal file
View File

@ -0,0 +1,541 @@
<?php
namespace Ds;
use Error;
use OutOfBoundsException;
use OutOfRangeException;
/**
* A sequence of unique values.
*
* @package Ds
*
* @template TValue
* @implements Collection<int, TValue>
* @implements \ArrayAccess<int, TValue>
* @template-use Traits\GenericCollection<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|null $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
* @return string
*/
public function join(string|null $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|null $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|null $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|null $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,200 @@
<?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 Collection<int, TValue>
* @implements \ArrayAccess<int, TValue>
* @template-use Traits\GenericCollection<int, 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,81 @@
<?php
namespace Ds\Traits;
/**
* Common to structures that implement the base collection interface.
* @template-covariant TKey
* @template-covariant TValue
*/
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.
*
* @psalm-return static<TKey, TValue>
*/
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
*
* @psalm-return array<TKey, TValue>
*/
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,447 @@
<?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
*
* @template TValue
*/
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
);
}
/**
* @return list<TValue>
*/
public function toArray(): array
{
return $this->array;
}
/**
* @psalm-param callable(TValue): TValue $callback
*/
public function apply(callable $callback)
{
foreach ($this->array as &$value) {
$value = $callback($value);
}
}
/**
* @template TValue2
* @psalm-param iterable<TValue2> $values
* @psalm-return Sequence<TValue|TValue2>
*/
public function merge($values): Sequence
{
$copy = $this->copy();
$copy->push(...$values);
return $copy;
}
/**
*
*/
public function count(): int
{
return count($this->array);
}
/**
* @psalm-param TValue ...$values
*/
public function contains(...$values): bool
{
foreach ($values as $value) {
if ($this->find($value) === null) {
return false;
}
}
return true;
}
/**
* @psalm-param (callable(TValue): bool)|null $callback
* @psalm-return Sequence<TValue>
*/
public function filter(callable|null $callback = null): Sequence
{
return new self(array_filter($this->array, $callback ?: 'boolval'));
}
/**
* @return int|null
*
* @psalm-param TValue $value
*/
public function find($value)
{
$offset = array_search($value, $this->array, true);
return $offset === false ? null : $offset;
}
/**
* @throws \UnderflowException if the sequence is empty.
*
* @psalm-return TValue
*/
public function first()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->array[0];
}
/**
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-return TValue
*/
public function get(int $index)
{
if ( ! $this->validIndex($index)) {
throw new OutOfRangeException();
}
return $this->array[$index];
}
/**
* @throws \OutOfRangeException if the index is not in the range [0, n]
*
* @psalm-param TValue ...$values
*/
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();
}
/**
*
*/
public function join(string $glue = null): string
{
return implode($glue ?? '', $this->array);
}
/**
* @throws \UnderflowException if the sequence is empty.
*
* @psalm-return TValue
*/
public function last()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
return $this->array[count($this) - 1];
}
/**
* @template TNewValue
* @psalm-param callable(TValue): TNewValue $callback
* @psalm-return Sequence<TNewValue>
*/
public function map(callable $callback): Sequence
{
return new self(array_map($callback, $this->array));
}
/**
* @throws \UnderflowException if the sequence is empty.
* @psalm-return TValue
*/
public function pop()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
$value = array_pop($this->array);
$this->checkCapacity();
return $value;
}
/**
* @psalm-param TValue ...$values
*/
public function push(...$values)
{
$this->ensureCapacity($this->count() + count($values));
foreach ($values as $value) {
$this->array[] = $value;
}
}
/**
* @template TCarry
* @psalm-param callable(TCarry, TValue): TCarry $callback
* @psalm-param TCarry $initial
* @psalm-return TCarry
*/
public function reduce(callable $callback, $initial = null)
{
return array_reduce($this->array, $callback, $initial);
}
/**
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-return TValue
*/
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;
}
/**
*
*/
public function reverse()
{
$this->array = array_reverse($this->array);
}
/**
* @psalm-return Sequence<TValue>
*/
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;
}
/**
*
*/
public function rotate(int $rotations)
{
for ($r = $this->normalizeRotations($rotations); $r > 0; $r--) {
array_push($this->array, array_shift($this->array));
}
}
/**
* @throws \OutOfRangeException if the index is not in the range [0, size-1]
*
* @psalm-param TValue $value
*/
public function set(int $index, $value)
{
if ( ! $this->validIndex($index)) {
throw new OutOfRangeException();
}
$this->array[$index] = $value;
}
/**
* @throws \UnderflowException if the sequence was empty.
*
* @psalm-return TValue
*/
public function shift()
{
if ($this->isEmpty()) {
throw new UnderflowException();
}
$value = array_shift($this->array);
$this->checkCapacity();
return $value;
}
/**
* @psalm-return Sequence<TValue>
*/
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));
}
/**
* @psalm-param (callable(TValue, TValue): int)|null $comparator
*/
public function sort(callable $comparator = null)
{
if ($comparator) {
usort($this->array, $comparator);
} else {
sort($this->array);
}
}
/**
* @psalm-param (callable(TValue, TValue): int)|null $comparator
* @psalm-return Sequence<TValue>
*/
public function sorted(callable $comparator = null): Sequence
{
$copy = $this->copy();
$copy->sort($comparator);
return $copy;
}
/**
* @return int|float
*/
public function sum()
{
return array_sum($this->array);
}
/**
* @psalm-param TValue ...$values
*/
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,37 @@
<?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>
* @template-use Traits\GenericCollection<int, TValue>
* @template-use Traits\GenericSequence<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;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Ds;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(E_ALL);

View File

@ -78,26 +78,27 @@ class CheckUpdate extends BasePluginRW
Log::info('开始检查项目更新');
// resource object
$offline = $this->fetchOfflineVersion();
$offline_version = $offline->get('version');
//
Log::info('拉取线上最新配置');
// object
$online = $this->fetchOnlineVersion();
$online_version = $online->version;
// 网络错误
if ($online->code != 200) {
Log::warning('检查更新: 拉取线上失败,网络错误!');
return false;
}
// 比较版本
if ($this->compareVersion($offline->get('version'), $online->version)) {
// TODO 完善消息 支持markdown
$time = $online->time;
$version = $online->version;
$des = $online->des;
$info = "请注意版本变动更新哦~\n\n版本号: $version\n\n更新日志: $des\n\n更新时间: $time\n\n";
if ($this->compareVersion($offline_version, $online_version)) {
//
$time = $online->update_time;
$desc = $online->update_description;
$info = "请注意版本变动更新哦~\n\n版本号: $online_version\n\n更新日志: $desc\n\n更新时间: $time\n\n";
Log::notice($info);
Notice::push('update', $info);
} else {
Log::info('程序已是最新版本');
Log::info("当前程序版本($offline_version)已是最新");
}
return true;
}

View File

@ -3,8 +3,8 @@ device_version: 0.0.1
app:
bili_a: # Android
package: "tv.danmaku.bili"
version: "8.27.0"
build: "8270400"
version: "8.33.0"
build: "8330200"
channel: "bili"
device: "phone"
mobi_app: "android"
@ -15,7 +15,7 @@ app:
secret_key: "NTYwYzUyY2NkMjg4ZmVkMDQ1ODU5ZWQxOGJmZmQ5NzM"
app_key_n: "NzgzYmJiNzI2NDQ1MWQ4Mg=="
secret_key_n: "MjY1MzU4M2M4ODczZGVhMjY4YWI5Mzg2OTE4YjFkNjU="
statistics: '{"appId":1,"platform":3,"version":"8.27.0","abtest":""}'
statistics: '{"appId":1,"platform":3,"version":"8.33.0","abtest":""}'
bili_i: # IOS
app_key: "MjdlYjUzZmM5MDU4ZjhjMw=="
secret_key: "YzJlZDUzYTc0ZWVlZmUzY2Y5OWZiZDAxZDhjOWMzNzU="

View File

@ -8,10 +8,7 @@
"dev_raw_url": "https://raw.githubusercontent.com/lkeme/BiliHelper-personal/dev/resources/version.json",
"master_purge_url": "https://cdn.staticaly.com/gh/lkeme/BiliHelper-personal/master/resources/version.json",
"dev_purge_url": "https://cdn.staticaly.com/gh/lkeme/BiliHelper-personal/dev/resources/version.json",
"version": "2.4.3.241231",
"des": "程序有更新,请及时线上查看更新哦~",
"time": "2024-12-31",
"ini_version": "0.0.1",
"ini_des": "配置有更新,请及时线上查看更新哦~",
"ini_time": "2024-12-31"
"version": "2.4.5.250218",
"update_time": "2025-02-18",
"update_description": "程序有更新,请及时线上查看更新哦~"
}

View File

@ -20,6 +20,11 @@ namespace Bhp\Util\Resource;
use Grasmash\Expander\Expander;
use Grasmash\Expander\Stringifier;
use JBZoo\Data\Data;
use JBZoo\Data\Ini;
use JBZoo\Data\JSON;
use JBZoo\Data\PhpArray;
use JBZoo\Data\Yml;
use Symfony\Component\Yaml\Yaml;
use function JBZoo\Data\data;
use function JBZoo\Data\ini;
use function JBZoo\Data\phpArray;
@ -35,9 +40,9 @@ class Resource extends Collection
protected const FORMAT_JSON = 'json';
/**
* @var Data
* @var Ini|JSON|Yml|PhpArray|Data
*/
protected Data $config;
protected Ini|JSON|Yml|PhpArray|Data $config;
/**
* @var string
@ -73,15 +78,15 @@ class Resource extends Collection
* 切换解析器
* @param string $filepath
* @param string $format
* @return Data
* @return Ini|JSON|Yml|PhpArray|Data
*/
protected function switchParser(string $filepath, string $format): Data
protected function switchParser(string $filepath, string $format): Ini|Json|Yml|PhpArray|Data
{
return match ($format) {
Resource::FORMAT_INI => ini($filepath),
Resource::FORMAT_PHP => phpArray($filepath),
Resource::FORMAT_YML, Resource::FORMAT_YAML => yml($filepath),
Resource::FORMAT_JSON => json($filepath),
Resource::FORMAT_YML, Resource::FORMAT_YAML => yml($filepath),
Resource::FORMAT_PHP => phpArray($filepath),
default => data($filepath),
};
}