| @@ -0,0 +1,5 @@ | |||
| { | |||
| "require": { | |||
| "ramsey/uuid": "^4.7" | |||
| } | |||
| } | |||
| @@ -0,0 +1,260 @@ | |||
| { | |||
| "_readme": [ | |||
| "This file locks the dependencies of your project to a known state", | |||
| "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | |||
| "This file is @generated automatically" | |||
| ], | |||
| "content-hash": "311c7a785a2af4ab4dae0f24542d289d", | |||
| "packages": [ | |||
| { | |||
| "name": "brick/math", | |||
| "version": "0.12.1", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/brick/math.git", | |||
| "reference": "f510c0a40911935b77b86859eb5223d58d660df1" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", | |||
| "reference": "f510c0a40911935b77b86859eb5223d58d660df1", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "php-coveralls/php-coveralls": "^2.2", | |||
| "phpunit/phpunit": "^10.1", | |||
| "vimeo/psalm": "5.16.0" | |||
| }, | |||
| "type": "library", | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Brick\\Math\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "description": "Arbitrary-precision arithmetic library", | |||
| "keywords": [ | |||
| "Arbitrary-precision", | |||
| "BigInteger", | |||
| "BigRational", | |||
| "arithmetic", | |||
| "bigdecimal", | |||
| "bignum", | |||
| "bignumber", | |||
| "brick", | |||
| "decimal", | |||
| "integer", | |||
| "math", | |||
| "mathematics", | |||
| "rational" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/brick/math/issues", | |||
| "source": "https://github.com/brick/math/tree/0.12.1" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/BenMorel", | |||
| "type": "github" | |||
| } | |||
| ], | |||
| "time": "2023-11-29T23:19:16+00:00" | |||
| }, | |||
| { | |||
| "name": "ramsey/collection", | |||
| "version": "2.0.0", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/ramsey/collection.git", | |||
| "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", | |||
| "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "ergebnis/composer-normalize": "^2.28.3", | |||
| "fakerphp/faker": "^1.21", | |||
| "hamcrest/hamcrest-php": "^2.0", | |||
| "jangregor/phpstan-prophecy": "^1.0", | |||
| "mockery/mockery": "^1.5", | |||
| "php-parallel-lint/php-console-highlighter": "^1.0", | |||
| "php-parallel-lint/php-parallel-lint": "^1.3", | |||
| "phpcsstandards/phpcsutils": "^1.0.0-rc1", | |||
| "phpspec/prophecy-phpunit": "^2.0", | |||
| "phpstan/extension-installer": "^1.2", | |||
| "phpstan/phpstan": "^1.9", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.3", | |||
| "phpunit/phpunit": "^9.5", | |||
| "psalm/plugin-mockery": "^1.1", | |||
| "psalm/plugin-phpunit": "^0.18.4", | |||
| "ramsey/coding-standard": "^2.0.3", | |||
| "ramsey/conventional-commits": "^1.3", | |||
| "vimeo/psalm": "^5.4" | |||
| }, | |||
| "type": "library", | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| }, | |||
| "ramsey/conventional-commits": { | |||
| "configFile": "conventional-commits.json" | |||
| } | |||
| }, | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Ramsey\\Collection\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "authors": [ | |||
| { | |||
| "name": "Ben Ramsey", | |||
| "email": "ben@benramsey.com", | |||
| "homepage": "https://benramsey.com" | |||
| } | |||
| ], | |||
| "description": "A PHP library for representing and manipulating collections.", | |||
| "keywords": [ | |||
| "array", | |||
| "collection", | |||
| "hash", | |||
| "map", | |||
| "queue", | |||
| "set" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/ramsey/collection/issues", | |||
| "source": "https://github.com/ramsey/collection/tree/2.0.0" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/ramsey", | |||
| "type": "github" | |||
| }, | |||
| { | |||
| "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", | |||
| "type": "tidelift" | |||
| } | |||
| ], | |||
| "time": "2022-12-31T21:50:55+00:00" | |||
| }, | |||
| { | |||
| "name": "ramsey/uuid", | |||
| "version": "4.7.6", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/ramsey/uuid.git", | |||
| "reference": "91039bc1faa45ba123c4328958e620d382ec7088" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", | |||
| "reference": "91039bc1faa45ba123c4328958e620d382ec7088", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", | |||
| "ext-json": "*", | |||
| "php": "^8.0", | |||
| "ramsey/collection": "^1.2 || ^2.0" | |||
| }, | |||
| "replace": { | |||
| "rhumsaa/uuid": "self.version" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/captainhook": "^5.10", | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", | |||
| "doctrine/annotations": "^1.8", | |||
| "ergebnis/composer-normalize": "^2.15", | |||
| "mockery/mockery": "^1.3", | |||
| "paragonie/random-lib": "^2", | |||
| "php-mock/php-mock": "^2.2", | |||
| "php-mock/php-mock-mockery": "^1.3", | |||
| "php-parallel-lint/php-parallel-lint": "^1.1", | |||
| "phpbench/phpbench": "^1.0", | |||
| "phpstan/extension-installer": "^1.1", | |||
| "phpstan/phpstan": "^1.8", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.1", | |||
| "phpunit/phpunit": "^8.5 || ^9", | |||
| "ramsey/composer-repl": "^1.4", | |||
| "slevomat/coding-standard": "^8.4", | |||
| "squizlabs/php_codesniffer": "^3.5", | |||
| "vimeo/psalm": "^4.9" | |||
| }, | |||
| "suggest": { | |||
| "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", | |||
| "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", | |||
| "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", | |||
| "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", | |||
| "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." | |||
| }, | |||
| "type": "library", | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| } | |||
| }, | |||
| "autoload": { | |||
| "files": [ | |||
| "src/functions.php" | |||
| ], | |||
| "psr-4": { | |||
| "Ramsey\\Uuid\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", | |||
| "keywords": [ | |||
| "guid", | |||
| "identifier", | |||
| "uuid" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/ramsey/uuid/issues", | |||
| "source": "https://github.com/ramsey/uuid/tree/4.7.6" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/ramsey", | |||
| "type": "github" | |||
| }, | |||
| { | |||
| "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", | |||
| "type": "tidelift" | |||
| } | |||
| ], | |||
| "time": "2024-04-27T21:32:50+00:00" | |||
| } | |||
| ], | |||
| "packages-dev": [], | |||
| "aliases": [], | |||
| "minimum-stability": "stable", | |||
| "stability-flags": [], | |||
| "prefer-stable": false, | |||
| "prefer-lowest": false, | |||
| "platform": [], | |||
| "platform-dev": [], | |||
| "plugin-api-version": "2.6.0" | |||
| } | |||
| @@ -432,6 +432,8 @@ app.core.Dict = { | |||
| "ACCOUNT_NOT_VALIDATED_DESCRIPTION" : "Bitte validiere zunächst deine Email Adresse. Wir haben dir gerade einen Validierungslink per Mail zugeschickt. Bitte schau in deinem Postfach nach (ggf. auch im Spam-Ordner) und klicke auf den Button.", | |||
| "LOGIN_FIRSTNAME" : "Vorname", | |||
| "LOGIN_LASTNAME" : "Nachname", | |||
| "DELETION_NOT_POSSIBLE_INFO_HEADLINE" : "Informationen zur Löschung deines Profils", | |||
| "DELETION_NOT_POSSIBLE_INFO" : "Du hast noch <strong>{0}</strong> Teilnahme(n) für zukünftige Termine. Bitte sage alle Teilnahmen ab, bevor Du Deinen Account löschen kannst.", | |||
| "DELETION_GROUP_OWNER_INFO_HEADLINE" : "Informationen zur Löschung deines Profils", | |||
| "DELETION_GROUP_OWNER_INFO" : "Bitte schreibe eine Mail an support@probuddy.de um die Löschung deines Profile bei Probuddy zu veranlassen. Da du Gruppeninhaber mindestens einer Gruppe bei ProBuddy bist, wird diese Gruppe/Gruppen automatisch mitgelöscht.", | |||
| "BTN_BACK" : "Zurück", | |||
| @@ -15,6 +15,7 @@ app.model.Appointment = function( appData, attendees, attendeeProfiles ) | |||
| attendees = attendees || [], | |||
| attendeeProfiles = attendeeProfiles || [], | |||
| id = data.id, | |||
| icon = data.icon, | |||
| category = data.category || null, | |||
| categoryIds = typeof( data.category_ids_js ) === 'string' ? JSON.parse( data.category_ids_js ) : data.category_ids_js, | |||
| visibility = data.visibility, | |||
| @@ -65,6 +66,11 @@ app.model.Appointment = function( appData, attendees, attendeeProfiles ) | |||
| return teamId; | |||
| }; | |||
| this.getIcon = function() | |||
| { | |||
| return icon; | |||
| }; | |||
| this.getSubject = function() | |||
| { | |||
| return subject; | |||
| @@ -184,6 +184,7 @@ app.state.AppointmentCreate = function() | |||
| var $form = $content.find( '[data-id="form-appointment"]' ).first(), | |||
| teamId, | |||
| icon, | |||
| appointmentState = $form.find( '[data-id="checkbox-appointment-state"]' ).first().is( ':checked' ) ? 'open' : null, | |||
| isValid = app.util.Form.bootstrapValidate( $form ); | |||
| @@ -191,11 +192,15 @@ app.state.AppointmentCreate = function() | |||
| { | |||
| app.gui.PageLoader.show(); | |||
| teamId = $form.find( '[data-id="select-team-id"]' ).first().val(); | |||
| icon = $('#input-subject-icon option:selected').val(); | |||
| //console.log(icon); | |||
| app.core.Rpc.call( | |||
| 'Appointment', | |||
| 'create', | |||
| { | |||
| teamId : teamId, | |||
| icon: icon, | |||
| categoryIds : $form.find( '[data-id="select-category-' + teamId + '"]' ).first().val(), | |||
| visibility : ( 'visibility-category-only' === $form.find( '[name="input-visibility"]:checked' ).first().val() ) ? app.model.Appointment.ENUM_VISIBLE_FOR_CATEGORIES : app.model.Appointment.ENUM_VISIBLE_FOR_ALL, | |||
| subject : $form.find( '[data-id="input-subject"]' ).first().val(), | |||
| @@ -168,6 +168,7 @@ app.state.AppointmentEdit= function() | |||
| 'Appointment', | |||
| 'update', | |||
| { | |||
| icon: $('#input-subject-icon option:selected').val(), | |||
| processSerial : isSerial, | |||
| appointmentId : appointmentId, | |||
| categoryIds : $form.find( '[data-id="select-category-' + appointment.getTeamId() + '"]' ).first().val(), | |||
| @@ -18,15 +18,18 @@ app.state.ConfigurationProfileDelete = function() | |||
| app.core.Rpc.call( | |||
| 'Account', | |||
| 'getAccountRelatedData', | |||
| null, | |||
| { | |||
| includeNumFutureAttendances : true | |||
| }, | |||
| function( res ) { | |||
| app.gui.PageLoader.hide(); | |||
| console.log(res); | |||
| app.core.View.setContent( | |||
| app.core.View.getTemplate( | |||
| 'configuration-profile-delete', | |||
| { | |||
| profile: new app.model.Profile(res.profile) | |||
| profile: new app.model.Profile(res.profile), | |||
| numFutureAttendances: res.numFutureAttendances | |||
| } | |||
| ) | |||
| ); | |||
| @@ -13,6 +13,7 @@ app.state.GroupMemberManagement = function() | |||
| { | |||
| let $content = app.core.View.getContent(), | |||
| group = null, | |||
| currentProfile = null, | |||
| memberToEdit = null, | |||
| members = [], | |||
| membersActive = [], | |||
| @@ -44,7 +44,7 @@ | |||
| <% } %> | |||
| </div> | |||
| <div class="appointment-detail-appointment-name"> | |||
| ❤ <%= a.getSubject() %> | |||
| <%= a.getIcon() %> <%= a.getSubject() %> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -29,20 +29,19 @@ | |||
| <label for="input-subject-icon"> | |||
| <%= _lc( 'APPOINTMENT_ICON' ) %> | |||
| </label> | |||
| <select class="form-control" | |||
| name="categoryicon" id="input-subject-icon"> | |||
| <option value="">- kein -</option> | |||
| <option value="⚘">⚘</option> | |||
| <option value="⚑">⚑</option> | |||
| <option value="✗">✗</option> | |||
| <option value="✩">✩</option> | |||
| <option value="✽">✽</option> | |||
| <option value="❤">❤</option> | |||
| <option value="➔">➔</option> | |||
| <option value="⚙">⚙</option> | |||
| <option value="⚇">⚇</option> | |||
| <option value="☼">☼</option> | |||
| <option value="☻">☻</option> | |||
| <select class="form-control" name="categoryicon" id="input-subject-icon"> | |||
| <option value="" <%= a.getIcon() === "" ? 'selected' : '' %>>- kein -</option> | |||
| <option value="⚘" <%= a.getIcon() === "❀" ? 'selected' : '' %>>⚘</option> | |||
| <option value="⚑" <%= a.getIcon() === "⚑" ? 'selected' : '' %>>⚑</option> | |||
| <option value="✗" <%= a.getIcon() === "✗" ? 'selected' : '' %>>✗</option> | |||
| <option value="✩" <%= a.getIcon() === "✩" ? 'selected' : '' %>>✩</option> | |||
| <option value="✽" <%= a.getIcon() === "✽" ? 'selected' : '' %>>✽</option> | |||
| <option value="❤" <%= a.getIcon() === "❤" ? 'selected' : '' %>>❤</option> | |||
| <option value="➔" <%= a.getIcon() === "➔" ? 'selected' : '' %>>➔</option> | |||
| <option value="⚙" <%= a.getIcon() === "⚙" ? 'selected' : '' %>>⚙</option> | |||
| <option value="⚇" <%= a.getIcon() === "⚇" ? 'selected' : '' %>>⚇</option> | |||
| <option value="☼" <%= a.getIcon() === "☼" ? 'selected' : '' %>>☼</option> | |||
| <option value="☻" <%= a.getIcon() === "☻" ? 'selected' : '' %>>☻</option> | |||
| </select> | |||
| </div> | |||
| <div class="col col-10"> | |||
| @@ -19,6 +19,23 @@ | |||
| </a> | |||
| </div> | |||
| <% } else if (numFutureAttendances > 0) { %> | |||
| <div class="card-header text-white bg-danger"> | |||
| <i class="fas fa-info-circle"></i> <%= _lc( 'DELETION_NOT_POSSIBLE_INFO_HEADLINE' ) %> | |||
| </div> | |||
| <div class="card-body"> | |||
| <p> | |||
| <strong><%=raw _lc( 'DELETION_NOT_POSSIBLE_INFO', [numFutureAttendances] ) %></strong> | |||
| </p> | |||
| </div> | |||
| <div class="card-footer"> | |||
| <a href="#/configuration/profile" | |||
| class="btn btn-sm btn-secondary"> | |||
| <%= _lc( 'BTN_BACK' ) %> | |||
| </a> | |||
| </div> | |||
| <% } else { %> | |||
| <form data-id="form-profile-deletion" | |||
| @@ -46,7 +46,7 @@ | |||
| <% } %> | |||
| </div> | |||
| <div class="appointment-subject"> | |||
| ❤ <%= a.getSubject() %> | |||
| <%= a.getIcon() %> <%= a.getSubject() %> | |||
| <% if ( a.isDraft() ) { %> | |||
| <span class="badge badge-warning">Entwurf</span> | |||
| <% } %> | |||
| @@ -0,0 +1,18 @@ | |||
| <?php | |||
| require_once __DIR__ . '/../server/config/boot_global.php'; | |||
| require_once __DIR__ . '/../server/config/boot_local.php'; | |||
| function patch() | |||
| { | |||
| $db = TB_Shared_Db_TeamData::get(); | |||
| $sql = "ALTER TABLE appointment | |||
| ADD COLUMN icon varchar(50) DEFAULT '' AFTER visibility;"; | |||
| $stmt = $db->query( $sql ); | |||
| echo "DONE..."; | |||
| } | |||
| patch(); | |||
| @@ -42,5 +42,18 @@ function _lc( $txt, array $params = NULL ) { | |||
| * | |||
| */ | |||
| function _xss( $txt ) { | |||
| return ( is_string( $txt ) ) ? filter_var( $txt, FILTER_SANITIZE_STRING ) : $txt; | |||
| if (!is_string($txt)) { | |||
| return $txt; | |||
| } | |||
| // Entfernt alle HTML- und PHP-Tags | |||
| $txt = strip_tags($txt); | |||
| // Konvertiert spezielle Zeichen in HTML-Entitäten | |||
| $txt = htmlspecialchars($txt, ENT_QUOTES | ENT_HTML5, 'UTF-8'); | |||
| // Entfernt Steuerzeichen | |||
| $txt = preg_replace('/[\x00-\x1F\x7F]/u', '', $txt); | |||
| return $txt; | |||
| } | |||
| @@ -3,6 +3,11 @@ | |||
| * (c)1337 aheadware.com - All rights reserved | |||
| ********************************************************************************/ | |||
| require Francis_Utils_Config::get( 'path.root' ).'/vendor/autoload.php'; | |||
| use Ramsey\Uuid\Uuid; | |||
| /** | |||
| * Class TB_Server_Control_Account | |||
| */ | |||
| @@ -23,13 +28,66 @@ class TB_Server_Control_Account | |||
| $resp->addData( 'profile', $session->getProfile() ); | |||
| $resp->addData( 'own_teams', TB_Shared_Ent_TeamData_Team::getByProfile( $session->getProfile() ) ); | |||
| if ($params->get('includeNumFutureAttendances')) { | |||
| $appointmentsAccepted = TB_Shared_Ent_TeamData_Appointment::getAcceptedByProfile( $session->getProfile()->id ); | |||
| $resp->addData( 'numFutureAttendances', count($appointmentsAccepted) ); | |||
| } | |||
| return $resp; | |||
| } | |||
| public static function delete( TB_Server_Core_RequestData $params ) | |||
| { | |||
| $resp = new TB_Server_Core_Response(); | |||
| $session = TB_Server_Core_Session::get(); | |||
| if ( is_null( $session ) ) { | |||
| throw new Exception( "No session found. Deletion failed." ); | |||
| } | |||
| $account = $session->getAccount(); | |||
| if ( is_null( $account ) ) { | |||
| throw new Exception( "No account in session. Deletion failed." ); | |||
| } | |||
| $profile = $session->getProfile(); | |||
| if ( is_null( $profile ) ) { | |||
| throw new Exception( "No profile. Deletion failed." ); | |||
| } | |||
| if ( $profile->ownsATeam() ) | |||
| { | |||
| throw new \Exception( 'Cannot delete a profile who owns a team' ); | |||
| } | |||
| $appointmentsAccepted = TB_Shared_Ent_TeamData_Appointment::getAcceptedByProfile( $profile->id ); | |||
| if ( $appointmentsAccepted && count( $appointmentsAccepted ) > 0 ) | |||
| { | |||
| throw new \Exception( 'User has accepted appointments in the future' ); | |||
| } | |||
| $uuid = \Ramsey\Uuid\Uuid::uuid4()->toString(); | |||
| throw new \Exception( 'Does not work anymore.' ); | |||
| return $resp; | |||
| // $dbCore = TB_Shared_Db_Core::get(); | |||
| // $dbTeam = TB_Shared_Db_TeamData::get(); | |||
| // | |||
| // | |||
| // if ( $appointmentsAccepted && count( $appointmentsAccepted ) > 0 ) | |||
| // { | |||
| // | |||
| // foreach( $appointmentsAccepted as $app ) | |||
| // { | |||
| // TB_Server_Control_Appointment::processWaitingList( $app ); | |||
| // } | |||
| // } | |||
| // | |||
| // return $resp; | |||
| /* | |||
| $resp = new TB_Server_Core_Response(); | |||
| @@ -25,7 +25,7 @@ class TB_Server_Control_Appointment { | |||
| { | |||
| throw new \Exception( 'No team id specified.' ); | |||
| } | |||
| $icon = _xss( $params->get( 'icon' ) ); | |||
| $sessionProfile = TB_Server_Core_Session::get()->getProfile(); | |||
| if ( false === $sessionProfile->isAdminOfTeam( $forTeamId ) ) | |||
| { | |||
| @@ -68,11 +68,15 @@ class TB_Server_Control_Appointment { | |||
| } | |||
| // Link replacements | |||
| if ( 0 === mb_strlen( $comment ) ) | |||
| { | |||
| $comment = NULL; | |||
| if (empty($comment) || !is_string($comment)) { | |||
| $comment = null; | |||
| } else { | |||
| $comment = TB_Server_Utils_Helper::makeUrltoLink($comment); | |||
| $comment = trim($comment); | |||
| if ($comment === '') { | |||
| $comment = null; | |||
| } else { | |||
| $comment = TB_Server_Utils_Helper::makeUrltoLink($comment); | |||
| } | |||
| } | |||
| $visibility = $params->get( 'visibility', TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL ); | |||
| @@ -121,6 +125,7 @@ class TB_Server_Control_Appointment { | |||
| $appointment = new TB_Shared_Ent_TeamData_Appointment(); | |||
| $appointment->team_id = $forTeamId; | |||
| $appointment->icon = $icon; | |||
| $appointment->category_ids_js = $categoryIds; | |||
| $appointment->visibility = $visibility; | |||
| $appointment->subject = _xss( $params->get( 'subject' ) ); | |||
| @@ -409,19 +414,19 @@ class TB_Server_Control_Appointment { | |||
| $comment = _xss( $params->get( 'comment' ) ); | |||
| if ( 0 === mb_strlen( $comment ) ) | |||
| { | |||
| $comment = NULL; | |||
| } | |||
| // Link replacements | |||
| if ( 0 === mb_strlen( $comment ) ) | |||
| { | |||
| $comment = NULL; | |||
| if (empty($comment) || !is_string($comment)) { | |||
| $comment = null; | |||
| } else { | |||
| $comment = TB_Server_Utils_Helper::makeUrltoLink($comment); | |||
| $comment = trim($comment); | |||
| if ($comment === '') { | |||
| $comment = null; | |||
| } else { | |||
| $comment = TB_Server_Utils_Helper::makeUrltoLink($comment); | |||
| } | |||
| } | |||
| $visibility = $params->get( 'visibility', TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL ); | |||
| if ( $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL && $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_CATEGORIES ) | |||
| @@ -497,6 +502,7 @@ class TB_Server_Control_Appointment { | |||
| $appointment->visibility = $visibility; | |||
| $appointment->category_ids_js = $categoryIds; | |||
| $appointment->subject = _xss( $params->get( 'subject' ) ); | |||
| $appointment->icon = _xss( $params->get( 'icon' ) ); | |||
| $appointment->start_dt->setTimezone( $defaultDtz ); | |||
| $appointment->end_dt->setTimezone( $defaultDtz ); | |||
| @@ -8,6 +8,7 @@ | |||
| * | |||
| * @property integer $id | |||
| * @property integer $team_id | |||
| * @property string $icon | |||
| * @property string $category_id | |||
| * @property array $category_ids_js | |||
| * @property string $visibility | |||
| @@ -149,9 +150,11 @@ class TB_Shared_Ent_TeamData_Appointment extends Francis_Db_Row | |||
| $sql .= 'attendee.status = "' . TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED . '" '; | |||
| if ( false === $includePastAppointments ) | |||
| { | |||
| $sql .= 'AND appointment.start_dt < UTC_TIMESTAMP()'; | |||
| $sql .= 'AND appointment.start_dt > UTC_TIMESTAMP()'; | |||
| } | |||
| //SELECT appointment.* FROM appointment LEFT JOIN attendee ON appointment.id = attendee.appointment_id WHERE appointment.state = "open" AND attendee.profile_id = 16772 AND attendee.status = "accepted"; | |||
| return self::findMany( $sql, array( ':profile_id' => $profileId ) ); | |||
| } | |||
| @@ -40,6 +40,7 @@ class TB_Shared_Ent_TeamData_Profile extends Francis_Db_Row { | |||
| const STATUS_NOT_APPROVED = 'not_approved'; | |||
| const STATUS_ACTIVE = 'active'; | |||
| const STATUS_INACTIVE = 'inactive'; | |||
| const STATUS_DELETED = 'deleted'; | |||
| // Enums | |||
| const GENDER_MALE = 'male'; | |||
| @@ -756,7 +757,7 @@ class TB_Shared_Ent_TeamData_Profile extends Francis_Db_Row { | |||
| */ | |||
| public static function getProfilesByTeamId( $teamId, $activeOnly = null ) | |||
| { | |||
| $params = array('team_id' => $teamId); | |||
| $params = array('team_id' => "$teamId"); | |||
| if ($activeOnly === true ) { | |||
| $params['status'] = self::STATUS_ACTIVE; | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| <?php | |||
| // autoload.php @generated by Composer | |||
| if (PHP_VERSION_ID < 50600) { | |||
| if (!headers_sent()) { | |||
| header('HTTP/1.1 500 Internal Server Error'); | |||
| } | |||
| $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; | |||
| if (!ini_get('display_errors')) { | |||
| if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { | |||
| fwrite(STDERR, $err); | |||
| } elseif (!headers_sent()) { | |||
| echo $err; | |||
| } | |||
| } | |||
| trigger_error( | |||
| $err, | |||
| E_USER_ERROR | |||
| ); | |||
| } | |||
| require_once __DIR__ . '/composer/autoload_real.php'; | |||
| return ComposerAutoloaderInit7422faf8f33d1f1cdfca8767e0ffc0f4::getLoader(); | |||
| @@ -0,0 +1,463 @@ | |||
| # Changelog | |||
| All notable changes to this project will be documented in this file. | |||
| ## [0.12.1](https://github.com/brick/math/releases/tag/0.12.1) - 2023-11-29 | |||
| ⚡️ **Performance improvements** | |||
| - `BigNumber::of()` is now faster, thanks to [@SebastienDug](https://github.com/SebastienDug) in [#77](https://github.com/brick/math/pull/77). | |||
| ## [0.12.0](https://github.com/brick/math/releases/tag/0.12.0) - 2023-11-26 | |||
| 💥 **Breaking changes** | |||
| - Minimum PHP version is now 8.1 | |||
| - `RoundingMode` is now an `enum`; if you're type-hinting rounding modes, you need to type-hint against `RoundingMode` instead of `int` now | |||
| - `BigNumber` classes do not implement the `Serializable` interface anymore (they use the [new custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization)) | |||
| - The following breaking changes only affect you if you're creating your own `BigNumber` subclasses: | |||
| - the return type of `BigNumber::of()` is now `static` | |||
| - `BigNumber` has a new abstract method `from()` | |||
| - all `public` and `protected` functions of `BigNumber` are now `final` | |||
| ## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16 | |||
| 💥 **Breaking changes** | |||
| - Minimum PHP version is now 8.0 | |||
| - Methods accepting a union of types are now strongly typed<sup>*</sup> | |||
| - `MathException` now extends `Exception` instead of `RuntimeException` | |||
| <sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods | |||
| internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string` | |||
| first.</sup> | |||
| ## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11 | |||
| 👌 **Improvements** | |||
| - `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic | |||
| ## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02 | |||
| ✨ **New features** | |||
| - `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers | |||
| ## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18 | |||
| 💥 **Breaking changes** | |||
| - Minimum PHP version is now 7.4 | |||
| ## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15 | |||
| 🚀 **Compatibility with PHP 8.1** | |||
| - Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (#60) thanks @TRowbotham | |||
| ## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20 | |||
| 🐛 **Bug fix** | |||
| - Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55). | |||
| ## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19 | |||
| ✨ **New features** | |||
| - `BigInteger::not()` returns the bitwise `NOT` value | |||
| 🐛 **Bug fixes** | |||
| - `BigInteger::toBytes()` could return an incorrect binary representation for some numbers | |||
| - The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available | |||
| ## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18 | |||
| 👌 **Improvements** | |||
| - `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal` | |||
| 💥 **Breaking changes** | |||
| - Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead | |||
| - Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead | |||
| ## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19 | |||
| 🐛 **Bug fix** | |||
| - `BigInteger::toBytes()` could return an incorrect binary representation for some numbers | |||
| - The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available | |||
| ## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18 | |||
| 🚑 **Critical fix** | |||
| - This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle. | |||
| ✨ **New features** | |||
| - `BigInteger::modInverse()` calculates a modular multiplicative inverse | |||
| - `BigInteger::fromBytes()` creates a `BigInteger` from a byte string | |||
| - `BigInteger::toBytes()` converts a `BigInteger` to a byte string | |||
| - `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length | |||
| - `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds | |||
| 💩 **Deprecations** | |||
| - `BigInteger::powerMod()` is now deprecated in favour of `modPow()` | |||
| ## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15 | |||
| 🐛 **Fixes** | |||
| - added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable` | |||
| ⚡️ **Optimizations** | |||
| - additional optimization in `BigInteger::remainder()` | |||
| ## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18 | |||
| ✨ **New features** | |||
| - `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit | |||
| ## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16 | |||
| ✨ **New features** | |||
| - `BigInteger::isEven()` tests whether the number is even | |||
| - `BigInteger::isOdd()` tests whether the number is odd | |||
| - `BigInteger::testBit()` tests if a bit is set | |||
| - `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number | |||
| ## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03 | |||
| 🛠️ **Maintenance release** | |||
| Classes are now annotated for better static analysis with [psalm](https://psalm.dev/). | |||
| This is a maintenance release: no bug fixes, no new features, no breaking changes. | |||
| ## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23 | |||
| ✨ **New feature** | |||
| `BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto. | |||
| ## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21 | |||
| ✨ **New feature** | |||
| `BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different. | |||
| ## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08 | |||
| ⚡️ **Performance improvements** | |||
| A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24. | |||
| ## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25 | |||
| 🐛 **Bug fixes** | |||
| - `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected) | |||
| ✨ **New features** | |||
| - `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet | |||
| - `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number | |||
| These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation. | |||
| 💩 **Deprecations** | |||
| - `BigInteger::parse()` is now deprecated in favour of `fromBase()` | |||
| `BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences: | |||
| - the `$base` parameter is required, it does not default to `10` | |||
| - it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed | |||
| ## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20 | |||
| **Improvements** | |||
| - Safer conversion from `float` when using custom locales | |||
| - **Much faster** `NativeCalculator` implementation 🚀 | |||
| You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before. | |||
| ## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11 | |||
| **New method** | |||
| `BigNumber::sum()` returns the sum of one or more numbers. | |||
| ## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12 | |||
| **Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20). | |||
| Thanks @manowark 👍 | |||
| ## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07 | |||
| **New method** | |||
| `BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale. | |||
| ## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06 | |||
| **New method** | |||
| `BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k). | |||
| **New exception** | |||
| `NegativeNumberException` is thrown when calling `sqrt()` on a negative number. | |||
| ## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08 | |||
| **Performance update** | |||
| - Further improvement of `toInt()` performance | |||
| - `NativeCalculator` can now perform some multiplications more efficiently | |||
| ## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07 | |||
| Performance optimization of `toInt()` methods. | |||
| ## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13 | |||
| **Breaking changes** | |||
| The following deprecated methods have been removed. Use the new method name instead: | |||
| | Method removed | Replacement method | | |||
| | --- | --- | | |||
| | `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` | | |||
| | `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` | | |||
| --- | |||
| **New features** | |||
| `BigInteger` has been augmented with 5 new methods for bitwise operations: | |||
| | New method | Description | | |||
| | --- | --- | | |||
| | `and()` | performs a bitwise `AND` operation on two numbers | | |||
| | `or()` | performs a bitwise `OR` operation on two numbers | | |||
| | `xor()` | performs a bitwise `XOR` operation on two numbers | | |||
| | `shiftedLeft()` | returns the number shifted left by a number of bits | | |||
| | `shiftedRight()` | returns the number shifted right by a number of bits | | |||
| Thanks to @DASPRiD 👍 | |||
| ## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20 | |||
| **New method:** `BigDecimal::hasNonZeroFractionalPart()` | |||
| **Renamed/deprecated methods:** | |||
| - `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated | |||
| - `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated | |||
| ## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21 | |||
| **Performance update** | |||
| `BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available. | |||
| ## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01 | |||
| This is a maintenance release, no code has been changed. | |||
| - When installed with `--no-dev`, the autoloader does not autoload tests anymore | |||
| - Tests and other files unnecessary for production are excluded from the dist package | |||
| This will help make installations more compact. | |||
| ## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02 | |||
| Methods renamed: | |||
| - `BigNumber:sign()` has been renamed to `getSign()` | |||
| - `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()` | |||
| - `BigDecimal::scale()` has been renamed to `getScale()` | |||
| - `BigDecimal::integral()` has been renamed to `getIntegral()` | |||
| - `BigDecimal::fraction()` has been renamed to `getFraction()` | |||
| - `BigRational::numerator()` has been renamed to `getNumerator()` | |||
| - `BigRational::denominator()` has been renamed to `getDenominator()` | |||
| Classes renamed: | |||
| - `ArithmeticException` has been renamed to `MathException` | |||
| ## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02 | |||
| The base class for all exceptions is now `MathException`. | |||
| `ArithmeticException` has been deprecated, and will be removed in 0.7.0. | |||
| ## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02 | |||
| A number of methods have been renamed: | |||
| - `BigNumber:sign()` is deprecated; use `getSign()` instead | |||
| - `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead | |||
| - `BigDecimal::scale()` is deprecated; use `getScale()` instead | |||
| - `BigDecimal::integral()` is deprecated; use `getIntegral()` instead | |||
| - `BigDecimal::fraction()` is deprecated; use `getFraction()` instead | |||
| - `BigRational::numerator()` is deprecated; use `getNumerator()` instead | |||
| - `BigRational::denominator()` is deprecated; use `getDenominator()` instead | |||
| The old methods will be removed in version 0.7.0. | |||
| ## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25 | |||
| - Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5` | |||
| - Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead | |||
| - Method `BigNumber::toInteger()` has been renamed to `toInt()` | |||
| ## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17 | |||
| `BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php). | |||
| The JSON output is always a string. | |||
| ## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31 | |||
| This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6. | |||
| ## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06 | |||
| The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again. | |||
| ## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05 | |||
| **New method: `BigNumber::toScale()`** | |||
| This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary. | |||
| ## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04 | |||
| **New features** | |||
| - Common `BigNumber` interface for all classes, with the following methods: | |||
| - `sign()` and derived methods (`isZero()`, `isPositive()`, ...) | |||
| - `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types | |||
| - `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods | |||
| - `toInteger()` and `toFloat()` conversion methods to native types | |||
| - Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type | |||
| - New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits | |||
| - New methods: `BigRational::quotient()` and `remainder()` | |||
| - Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException` | |||
| - Factory methods `zero()`, `one()` and `ten()` available in all classes | |||
| - Rounding mode reintroduced in `BigInteger::dividedBy()` | |||
| This release also comes with many performance improvements. | |||
| --- | |||
| **Breaking changes** | |||
| - `BigInteger`: | |||
| - `getSign()` is renamed to `sign()` | |||
| - `toString()` is renamed to `toBase()` | |||
| - `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour | |||
| - `BigDecimal`: | |||
| - `getSign()` is renamed to `sign()` | |||
| - `getUnscaledValue()` is renamed to `unscaledValue()` | |||
| - `getScale()` is renamed to `scale()` | |||
| - `getIntegral()` is renamed to `integral()` | |||
| - `getFraction()` is renamed to `fraction()` | |||
| - `divideAndRemainder()` is renamed to `quotientAndRemainder()` | |||
| - `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode | |||
| - `toBigInteger()` does not accept a `$roundingMode` parameter anymore | |||
| - `toBigRational()` does not simplify the fraction anymore; explicitly add `->simplified()` to get the previous behaviour | |||
| - `BigRational`: | |||
| - `getSign()` is renamed to `sign()` | |||
| - `getNumerator()` is renamed to `numerator()` | |||
| - `getDenominator()` is renamed to `denominator()` | |||
| - `of()` is renamed to `nd()`, while `parse()` is renamed to `of()` | |||
| - Miscellaneous: | |||
| - `ArithmeticException` is moved to an `Exception\` sub-namespace | |||
| - `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException` | |||
| ## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31 | |||
| Backport of two bug fixes from the 0.5 branch: | |||
| - `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected | |||
| - Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6. | |||
| ## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16 | |||
| New method: `BigDecimal::stripTrailingZeros()` | |||
| ## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12 | |||
| Introducing a `BigRational` class, to perform calculations on fractions of any size. | |||
| ## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12 | |||
| Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`. | |||
| `BigInteger::dividedBy()` now always returns the quotient of the division. | |||
| ## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31 | |||
| Backport of two bug fixes from the 0.5 branch: | |||
| - `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected | |||
| - Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6. | |||
| ## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11 | |||
| New methods: | |||
| - `BigInteger::remainder()` returns the remainder of a division only | |||
| - `BigInteger::gcd()` returns the greatest common divisor of two numbers | |||
| ## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07 | |||
| Fix `toString()` not handling negative numbers. | |||
| ## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07 | |||
| `BigInteger` and `BigDecimal` now have a `getSign()` method that returns: | |||
| - `-1` if the number is negative | |||
| - `0` if the number is zero | |||
| - `1` if the number is positive | |||
| ## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05 | |||
| Minor performance improvements | |||
| ## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04 | |||
| The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`. | |||
| ## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04 | |||
| Stronger immutability guarantee for `BigInteger` and `BigDecimal`. | |||
| So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that. | |||
| ## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02 | |||
| Added `BigDecimal::divideAndRemainder()` | |||
| ## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22 | |||
| - `min()` and `max()` do not accept an `array` anymore, but a variable number of parameters | |||
| - **minimum PHP version is now 5.6** | |||
| - continuous integration with PHP 7 | |||
| ## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01 | |||
| - Added `BigInteger::power()` | |||
| - Added HHVM support | |||
| ## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31 | |||
| First beta release. | |||
| @@ -0,0 +1,20 @@ | |||
| The MIT License (MIT) | |||
| Copyright (c) 2013-present Benjamin Morel | |||
| 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. | |||
| @@ -0,0 +1,39 @@ | |||
| { | |||
| "name": "brick/math", | |||
| "description": "Arbitrary-precision arithmetic library", | |||
| "type": "library", | |||
| "keywords": [ | |||
| "Brick", | |||
| "Math", | |||
| "Mathematics", | |||
| "Arbitrary-precision", | |||
| "Arithmetic", | |||
| "BigInteger", | |||
| "BigDecimal", | |||
| "BigRational", | |||
| "BigNumber", | |||
| "Bignum", | |||
| "Decimal", | |||
| "Rational", | |||
| "Integer" | |||
| ], | |||
| "license": "MIT", | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "phpunit/phpunit": "^10.1", | |||
| "php-coveralls/php-coveralls": "^2.2", | |||
| "vimeo/psalm": "5.16.0" | |||
| }, | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Brick\\Math\\": "src/" | |||
| } | |||
| }, | |||
| "autoload-dev": { | |||
| "psr-4": { | |||
| "Brick\\Math\\Tests\\": "tests/" | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,754 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math; | |||
| use Brick\Math\Exception\DivisionByZeroException; | |||
| use Brick\Math\Exception\MathException; | |||
| use Brick\Math\Exception\NegativeNumberException; | |||
| use Brick\Math\Internal\Calculator; | |||
| /** | |||
| * Immutable, arbitrary-precision signed decimal numbers. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| final class BigDecimal extends BigNumber | |||
| { | |||
| /** | |||
| * The unscaled value of this decimal number. | |||
| * | |||
| * This is a string of digits with an optional leading minus sign. | |||
| * No leading zero must be present. | |||
| * No leading minus sign must be present if the value is 0. | |||
| */ | |||
| private readonly string $value; | |||
| /** | |||
| * The scale (number of digits after the decimal point) of this decimal number. | |||
| * | |||
| * This must be zero or more. | |||
| */ | |||
| private readonly int $scale; | |||
| /** | |||
| * Protected constructor. Use a factory method to obtain an instance. | |||
| * | |||
| * @param string $value The unscaled value, validated. | |||
| * @param int $scale The scale, validated. | |||
| */ | |||
| protected function __construct(string $value, int $scale = 0) | |||
| { | |||
| $this->value = $value; | |||
| $this->scale = $scale; | |||
| } | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| protected static function from(BigNumber $number): static | |||
| { | |||
| return $number->toBigDecimal(); | |||
| } | |||
| /** | |||
| * Creates a BigDecimal from an unscaled value and a scale. | |||
| * | |||
| * Example: `(12345, 3)` will result in the BigDecimal `12.345`. | |||
| * | |||
| * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger. | |||
| * @param int $scale The scale of the number, positive or zero. | |||
| * | |||
| * @throws \InvalidArgumentException If the scale is negative. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal | |||
| { | |||
| if ($scale < 0) { | |||
| throw new \InvalidArgumentException('The scale cannot be negative.'); | |||
| } | |||
| return new BigDecimal((string) BigInteger::of($value), $scale); | |||
| } | |||
| /** | |||
| * Returns a BigDecimal representing zero, with a scale of zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function zero() : BigDecimal | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigDecimal|null $zero | |||
| */ | |||
| static $zero; | |||
| if ($zero === null) { | |||
| $zero = new BigDecimal('0'); | |||
| } | |||
| return $zero; | |||
| } | |||
| /** | |||
| * Returns a BigDecimal representing one, with a scale of zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function one() : BigDecimal | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigDecimal|null $one | |||
| */ | |||
| static $one; | |||
| if ($one === null) { | |||
| $one = new BigDecimal('1'); | |||
| } | |||
| return $one; | |||
| } | |||
| /** | |||
| * Returns a BigDecimal representing ten, with a scale of zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function ten() : BigDecimal | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigDecimal|null $ten | |||
| */ | |||
| static $ten; | |||
| if ($ten === null) { | |||
| $ten = new BigDecimal('10'); | |||
| } | |||
| return $ten; | |||
| } | |||
| /** | |||
| * Returns the sum of this number and the given one. | |||
| * | |||
| * The result has a scale of `max($this->scale, $that->scale)`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. | |||
| */ | |||
| public function plus(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->value === '0' && $that->scale <= $this->scale) { | |||
| return $this; | |||
| } | |||
| if ($this->value === '0' && $this->scale <= $that->scale) { | |||
| return $that; | |||
| } | |||
| [$a, $b] = $this->scaleValues($this, $that); | |||
| $value = Calculator::get()->add($a, $b); | |||
| $scale = $this->scale > $that->scale ? $this->scale : $that->scale; | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns the difference of this number and the given one. | |||
| * | |||
| * The result has a scale of `max($this->scale, $that->scale)`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. | |||
| */ | |||
| public function minus(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->value === '0' && $that->scale <= $this->scale) { | |||
| return $this; | |||
| } | |||
| [$a, $b] = $this->scaleValues($this, $that); | |||
| $value = Calculator::get()->sub($a, $b); | |||
| $scale = $this->scale > $that->scale ? $this->scale : $that->scale; | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns the product of this number and the given one. | |||
| * | |||
| * The result has a scale of `$this->scale + $that->scale`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. | |||
| */ | |||
| public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->value === '1' && $that->scale === 0) { | |||
| return $this; | |||
| } | |||
| if ($this->value === '1' && $this->scale === 0) { | |||
| return $that; | |||
| } | |||
| $value = Calculator::get()->mul($this->value, $that->value); | |||
| $scale = $this->scale + $that->scale; | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns the result of the division of this number by the given one, at the given scale. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. | |||
| * @param int|null $scale The desired scale, or null to use the scale of this number. | |||
| * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. | |||
| * | |||
| * @throws \InvalidArgumentException If the scale or rounding mode is invalid. | |||
| * @throws MathException If the number is invalid, is zero, or rounding was necessary. | |||
| */ | |||
| public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->isZero()) { | |||
| throw DivisionByZeroException::divisionByZero(); | |||
| } | |||
| if ($scale === null) { | |||
| $scale = $this->scale; | |||
| } elseif ($scale < 0) { | |||
| throw new \InvalidArgumentException('Scale cannot be negative.'); | |||
| } | |||
| if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) { | |||
| return $this; | |||
| } | |||
| $p = $this->valueWithMinScale($that->scale + $scale); | |||
| $q = $that->valueWithMinScale($this->scale - $scale); | |||
| $result = Calculator::get()->divRound($p, $q, $roundingMode); | |||
| return new BigDecimal($result, $scale); | |||
| } | |||
| /** | |||
| * Returns the exact result of the division of this number by the given one. | |||
| * | |||
| * The scale of the result is automatically calculated to fit all the fraction digits. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero, | |||
| * or the result yields an infinite number of digits. | |||
| */ | |||
| public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->value === '0') { | |||
| throw DivisionByZeroException::divisionByZero(); | |||
| } | |||
| [, $b] = $this->scaleValues($this, $that); | |||
| $d = \rtrim($b, '0'); | |||
| $scale = \strlen($b) - \strlen($d); | |||
| $calculator = Calculator::get(); | |||
| foreach ([5, 2] as $prime) { | |||
| for (;;) { | |||
| $lastDigit = (int) $d[-1]; | |||
| if ($lastDigit % $prime !== 0) { | |||
| break; | |||
| } | |||
| $d = $calculator->divQ($d, (string) $prime); | |||
| $scale++; | |||
| } | |||
| } | |||
| return $this->dividedBy($that, $scale)->stripTrailingZeros(); | |||
| } | |||
| /** | |||
| * Returns this number exponentiated to the given value. | |||
| * | |||
| * The result has a scale of `$this->scale * $exponent`. | |||
| * | |||
| * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. | |||
| */ | |||
| public function power(int $exponent) : BigDecimal | |||
| { | |||
| if ($exponent === 0) { | |||
| return BigDecimal::one(); | |||
| } | |||
| if ($exponent === 1) { | |||
| return $this; | |||
| } | |||
| if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { | |||
| throw new \InvalidArgumentException(\sprintf( | |||
| 'The exponent %d is not in the range 0 to %d.', | |||
| $exponent, | |||
| Calculator::MAX_POWER | |||
| )); | |||
| } | |||
| return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent); | |||
| } | |||
| /** | |||
| * Returns the quotient of the division of this number by the given one. | |||
| * | |||
| * The quotient has a scale of `0`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the divisor is not a valid decimal number, or is zero. | |||
| */ | |||
| public function quotient(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->isZero()) { | |||
| throw DivisionByZeroException::divisionByZero(); | |||
| } | |||
| $p = $this->valueWithMinScale($that->scale); | |||
| $q = $that->valueWithMinScale($this->scale); | |||
| $quotient = Calculator::get()->divQ($p, $q); | |||
| return new BigDecimal($quotient, 0); | |||
| } | |||
| /** | |||
| * Returns the remainder of the division of this number by the given one. | |||
| * | |||
| * The remainder has a scale of `max($this->scale, $that->scale)`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. | |||
| * | |||
| * @throws MathException If the divisor is not a valid decimal number, or is zero. | |||
| */ | |||
| public function remainder(BigNumber|int|float|string $that) : BigDecimal | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->isZero()) { | |||
| throw DivisionByZeroException::divisionByZero(); | |||
| } | |||
| $p = $this->valueWithMinScale($that->scale); | |||
| $q = $that->valueWithMinScale($this->scale); | |||
| $remainder = Calculator::get()->divR($p, $q); | |||
| $scale = $this->scale > $that->scale ? $this->scale : $that->scale; | |||
| return new BigDecimal($remainder, $scale); | |||
| } | |||
| /** | |||
| * Returns the quotient and remainder of the division of this number by the given one. | |||
| * | |||
| * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. | |||
| * | |||
| * @return BigDecimal[] An array containing the quotient and the remainder. | |||
| * | |||
| * @psalm-return array{BigDecimal, BigDecimal} | |||
| * | |||
| * @throws MathException If the divisor is not a valid decimal number, or is zero. | |||
| */ | |||
| public function quotientAndRemainder(BigNumber|int|float|string $that) : array | |||
| { | |||
| $that = BigDecimal::of($that); | |||
| if ($that->isZero()) { | |||
| throw DivisionByZeroException::divisionByZero(); | |||
| } | |||
| $p = $this->valueWithMinScale($that->scale); | |||
| $q = $that->valueWithMinScale($this->scale); | |||
| [$quotient, $remainder] = Calculator::get()->divQR($p, $q); | |||
| $scale = $this->scale > $that->scale ? $this->scale : $that->scale; | |||
| $quotient = new BigDecimal($quotient, 0); | |||
| $remainder = new BigDecimal($remainder, $scale); | |||
| return [$quotient, $remainder]; | |||
| } | |||
| /** | |||
| * Returns the square root of this number, rounded down to the given number of decimals. | |||
| * | |||
| * @throws \InvalidArgumentException If the scale is negative. | |||
| * @throws NegativeNumberException If this number is negative. | |||
| */ | |||
| public function sqrt(int $scale) : BigDecimal | |||
| { | |||
| if ($scale < 0) { | |||
| throw new \InvalidArgumentException('Scale cannot be negative.'); | |||
| } | |||
| if ($this->value === '0') { | |||
| return new BigDecimal('0', $scale); | |||
| } | |||
| if ($this->value[0] === '-') { | |||
| throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); | |||
| } | |||
| $value = $this->value; | |||
| $addDigits = 2 * $scale - $this->scale; | |||
| if ($addDigits > 0) { | |||
| // add zeros | |||
| $value .= \str_repeat('0', $addDigits); | |||
| } elseif ($addDigits < 0) { | |||
| // trim digits | |||
| if (-$addDigits >= \strlen($this->value)) { | |||
| // requesting a scale too low, will always yield a zero result | |||
| return new BigDecimal('0', $scale); | |||
| } | |||
| $value = \substr($value, 0, $addDigits); | |||
| } | |||
| $value = Calculator::get()->sqrt($value); | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns a copy of this BigDecimal with the decimal point moved $n places to the left. | |||
| */ | |||
| public function withPointMovedLeft(int $n) : BigDecimal | |||
| { | |||
| if ($n === 0) { | |||
| return $this; | |||
| } | |||
| if ($n < 0) { | |||
| return $this->withPointMovedRight(-$n); | |||
| } | |||
| return new BigDecimal($this->value, $this->scale + $n); | |||
| } | |||
| /** | |||
| * Returns a copy of this BigDecimal with the decimal point moved $n places to the right. | |||
| */ | |||
| public function withPointMovedRight(int $n) : BigDecimal | |||
| { | |||
| if ($n === 0) { | |||
| return $this; | |||
| } | |||
| if ($n < 0) { | |||
| return $this->withPointMovedLeft(-$n); | |||
| } | |||
| $value = $this->value; | |||
| $scale = $this->scale - $n; | |||
| if ($scale < 0) { | |||
| if ($value !== '0') { | |||
| $value .= \str_repeat('0', -$scale); | |||
| } | |||
| $scale = 0; | |||
| } | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part. | |||
| */ | |||
| public function stripTrailingZeros() : BigDecimal | |||
| { | |||
| if ($this->scale === 0) { | |||
| return $this; | |||
| } | |||
| $trimmedValue = \rtrim($this->value, '0'); | |||
| if ($trimmedValue === '') { | |||
| return BigDecimal::zero(); | |||
| } | |||
| $trimmableZeros = \strlen($this->value) - \strlen($trimmedValue); | |||
| if ($trimmableZeros === 0) { | |||
| return $this; | |||
| } | |||
| if ($trimmableZeros > $this->scale) { | |||
| $trimmableZeros = $this->scale; | |||
| } | |||
| $value = \substr($this->value, 0, -$trimmableZeros); | |||
| $scale = $this->scale - $trimmableZeros; | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Returns the absolute value of this number. | |||
| */ | |||
| public function abs() : BigDecimal | |||
| { | |||
| return $this->isNegative() ? $this->negated() : $this; | |||
| } | |||
| /** | |||
| * Returns the negated value of this number. | |||
| */ | |||
| public function negated() : BigDecimal | |||
| { | |||
| return new BigDecimal(Calculator::get()->neg($this->value), $this->scale); | |||
| } | |||
| public function compareTo(BigNumber|int|float|string $that) : int | |||
| { | |||
| $that = BigNumber::of($that); | |||
| if ($that instanceof BigInteger) { | |||
| $that = $that->toBigDecimal(); | |||
| } | |||
| if ($that instanceof BigDecimal) { | |||
| [$a, $b] = $this->scaleValues($this, $that); | |||
| return Calculator::get()->cmp($a, $b); | |||
| } | |||
| return - $that->compareTo($this); | |||
| } | |||
| public function getSign() : int | |||
| { | |||
| return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); | |||
| } | |||
| public function getUnscaledValue() : BigInteger | |||
| { | |||
| return self::newBigInteger($this->value); | |||
| } | |||
| public function getScale() : int | |||
| { | |||
| return $this->scale; | |||
| } | |||
| /** | |||
| * Returns a string representing the integral part of this decimal number. | |||
| * | |||
| * Example: `-123.456` => `-123`. | |||
| */ | |||
| public function getIntegralPart() : string | |||
| { | |||
| if ($this->scale === 0) { | |||
| return $this->value; | |||
| } | |||
| $value = $this->getUnscaledValueWithLeadingZeros(); | |||
| return \substr($value, 0, -$this->scale); | |||
| } | |||
| /** | |||
| * Returns a string representing the fractional part of this decimal number. | |||
| * | |||
| * If the scale is zero, an empty string is returned. | |||
| * | |||
| * Examples: `-123.456` => '456', `123` => ''. | |||
| */ | |||
| public function getFractionalPart() : string | |||
| { | |||
| if ($this->scale === 0) { | |||
| return ''; | |||
| } | |||
| $value = $this->getUnscaledValueWithLeadingZeros(); | |||
| return \substr($value, -$this->scale); | |||
| } | |||
| /** | |||
| * Returns whether this decimal number has a non-zero fractional part. | |||
| */ | |||
| public function hasNonZeroFractionalPart() : bool | |||
| { | |||
| return $this->getFractionalPart() !== \str_repeat('0', $this->scale); | |||
| } | |||
| public function toBigInteger() : BigInteger | |||
| { | |||
| $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); | |||
| return self::newBigInteger($zeroScaleDecimal->value); | |||
| } | |||
| public function toBigDecimal() : BigDecimal | |||
| { | |||
| return $this; | |||
| } | |||
| public function toBigRational() : BigRational | |||
| { | |||
| $numerator = self::newBigInteger($this->value); | |||
| $denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale)); | |||
| return self::newBigRational($numerator, $denominator, false); | |||
| } | |||
| public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal | |||
| { | |||
| if ($scale === $this->scale) { | |||
| return $this; | |||
| } | |||
| return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode); | |||
| } | |||
| public function toInt() : int | |||
| { | |||
| return $this->toBigInteger()->toInt(); | |||
| } | |||
| public function toFloat() : float | |||
| { | |||
| return (float) (string) $this; | |||
| } | |||
| public function __toString() : string | |||
| { | |||
| if ($this->scale === 0) { | |||
| return $this->value; | |||
| } | |||
| $value = $this->getUnscaledValueWithLeadingZeros(); | |||
| return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale); | |||
| } | |||
| /** | |||
| * This method is required for serializing the object and SHOULD NOT be accessed directly. | |||
| * | |||
| * @internal | |||
| * | |||
| * @return array{value: string, scale: int} | |||
| */ | |||
| public function __serialize(): array | |||
| { | |||
| return ['value' => $this->value, 'scale' => $this->scale]; | |||
| } | |||
| /** | |||
| * This method is only here to allow unserializing the object and cannot be accessed directly. | |||
| * | |||
| * @internal | |||
| * @psalm-suppress RedundantPropertyInitializationCheck | |||
| * | |||
| * @param array{value: string, scale: int} $data | |||
| * | |||
| * @throws \LogicException | |||
| */ | |||
| public function __unserialize(array $data): void | |||
| { | |||
| if (isset($this->value)) { | |||
| throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); | |||
| } | |||
| $this->value = $data['value']; | |||
| $this->scale = $data['scale']; | |||
| } | |||
| /** | |||
| * Puts the internal values of the given decimal numbers on the same scale. | |||
| * | |||
| * @return array{string, string} The scaled integer values of $x and $y. | |||
| */ | |||
| private function scaleValues(BigDecimal $x, BigDecimal $y) : array | |||
| { | |||
| $a = $x->value; | |||
| $b = $y->value; | |||
| if ($b !== '0' && $x->scale > $y->scale) { | |||
| $b .= \str_repeat('0', $x->scale - $y->scale); | |||
| } elseif ($a !== '0' && $x->scale < $y->scale) { | |||
| $a .= \str_repeat('0', $y->scale - $x->scale); | |||
| } | |||
| return [$a, $b]; | |||
| } | |||
| private function valueWithMinScale(int $scale) : string | |||
| { | |||
| $value = $this->value; | |||
| if ($this->value !== '0' && $scale > $this->scale) { | |||
| $value .= \str_repeat('0', $scale - $this->scale); | |||
| } | |||
| return $value; | |||
| } | |||
| /** | |||
| * Adds leading zeros if necessary to the unscaled value to represent the full decimal number. | |||
| */ | |||
| private function getUnscaledValueWithLeadingZeros() : string | |||
| { | |||
| $value = $this->value; | |||
| $targetLength = $this->scale + 1; | |||
| $negative = ($value[0] === '-'); | |||
| $length = \strlen($value); | |||
| if ($negative) { | |||
| $length--; | |||
| } | |||
| if ($length >= $targetLength) { | |||
| return $this->value; | |||
| } | |||
| if ($negative) { | |||
| $value = \substr($value, 1); | |||
| } | |||
| $value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT); | |||
| if ($negative) { | |||
| $value = '-' . $value; | |||
| } | |||
| return $value; | |||
| } | |||
| } | |||
| @@ -0,0 +1,509 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math; | |||
| use Brick\Math\Exception\DivisionByZeroException; | |||
| use Brick\Math\Exception\MathException; | |||
| use Brick\Math\Exception\NumberFormatException; | |||
| use Brick\Math\Exception\RoundingNecessaryException; | |||
| /** | |||
| * Common interface for arbitrary-precision rational numbers. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| abstract class BigNumber implements \JsonSerializable | |||
| { | |||
| /** | |||
| * The regular expression used to parse integer or decimal numbers. | |||
| */ | |||
| private const PARSE_REGEXP_NUMERICAL = | |||
| '/^' . | |||
| '(?<sign>[\-\+])?' . | |||
| '(?<integral>[0-9]+)?' . | |||
| '(?<point>\.)?' . | |||
| '(?<fractional>[0-9]+)?' . | |||
| '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' . | |||
| '$/'; | |||
| /** | |||
| * The regular expression used to parse rational numbers. | |||
| */ | |||
| private const PARSE_REGEXP_RATIONAL = | |||
| '/^' . | |||
| '(?<sign>[\-\+])?' . | |||
| '(?<numerator>[0-9]+)' . | |||
| '\/?' . | |||
| '(?<denominator>[0-9]+)' . | |||
| '$/'; | |||
| /** | |||
| * Creates a BigNumber of the given value. | |||
| * | |||
| * The concrete return type is dependent on the given value, with the following rules: | |||
| * | |||
| * - BigNumber instances are returned as is | |||
| * - integer numbers are returned as BigInteger | |||
| * - floating point numbers are converted to a string then parsed as such | |||
| * - strings containing a `/` character are returned as BigRational | |||
| * - strings containing a `.` character or using an exponential notation are returned as BigDecimal | |||
| * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger | |||
| * | |||
| * @throws NumberFormatException If the format of the number is not valid. | |||
| * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| final public static function of(BigNumber|int|float|string $value) : static | |||
| { | |||
| $value = self::_of($value); | |||
| if (static::class === BigNumber::class) { | |||
| // https://github.com/vimeo/psalm/issues/10309 | |||
| assert($value instanceof static); | |||
| return $value; | |||
| } | |||
| return static::from($value); | |||
| } | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| private static function _of(BigNumber|int|float|string $value) : BigNumber | |||
| { | |||
| if ($value instanceof BigNumber) { | |||
| return $value; | |||
| } | |||
| if (\is_int($value)) { | |||
| return new BigInteger((string) $value); | |||
| } | |||
| if (is_float($value)) { | |||
| $value = (string) $value; | |||
| } | |||
| if (str_contains($value, '/')) { | |||
| // Rational number | |||
| if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { | |||
| throw NumberFormatException::invalidFormat($value); | |||
| } | |||
| $sign = $matches['sign']; | |||
| $numerator = $matches['numerator']; | |||
| $denominator = $matches['denominator']; | |||
| assert($numerator !== null); | |||
| assert($denominator !== null); | |||
| $numerator = self::cleanUp($sign, $numerator); | |||
| $denominator = self::cleanUp(null, $denominator); | |||
| if ($denominator === '0') { | |||
| throw DivisionByZeroException::denominatorMustNotBeZero(); | |||
| } | |||
| return new BigRational( | |||
| new BigInteger($numerator), | |||
| new BigInteger($denominator), | |||
| false | |||
| ); | |||
| } else { | |||
| // Integer or decimal number | |||
| if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { | |||
| throw NumberFormatException::invalidFormat($value); | |||
| } | |||
| $sign = $matches['sign']; | |||
| $point = $matches['point']; | |||
| $integral = $matches['integral']; | |||
| $fractional = $matches['fractional']; | |||
| $exponent = $matches['exponent']; | |||
| if ($integral === null && $fractional === null) { | |||
| throw NumberFormatException::invalidFormat($value); | |||
| } | |||
| if ($integral === null) { | |||
| $integral = '0'; | |||
| } | |||
| if ($point !== null || $exponent !== null) { | |||
| $fractional = ($fractional ?? ''); | |||
| $exponent = ($exponent !== null) ? (int)$exponent : 0; | |||
| if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { | |||
| throw new NumberFormatException('Exponent too large.'); | |||
| } | |||
| $unscaledValue = self::cleanUp($sign, $integral . $fractional); | |||
| $scale = \strlen($fractional) - $exponent; | |||
| if ($scale < 0) { | |||
| if ($unscaledValue !== '0') { | |||
| $unscaledValue .= \str_repeat('0', -$scale); | |||
| } | |||
| $scale = 0; | |||
| } | |||
| return new BigDecimal($unscaledValue, $scale); | |||
| } | |||
| $integral = self::cleanUp($sign, $integral); | |||
| return new BigInteger($integral); | |||
| } | |||
| } | |||
| /** | |||
| * Overridden by subclasses to convert a BigNumber to an instance of the subclass. | |||
| * | |||
| * @throws MathException If the value cannot be converted. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| abstract protected static function from(BigNumber $number): static; | |||
| /** | |||
| * Proxy method to access BigInteger's protected constructor from sibling classes. | |||
| * | |||
| * @internal | |||
| * @psalm-pure | |||
| */ | |||
| final protected function newBigInteger(string $value) : BigInteger | |||
| { | |||
| return new BigInteger($value); | |||
| } | |||
| /** | |||
| * Proxy method to access BigDecimal's protected constructor from sibling classes. | |||
| * | |||
| * @internal | |||
| * @psalm-pure | |||
| */ | |||
| final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal | |||
| { | |||
| return new BigDecimal($value, $scale); | |||
| } | |||
| /** | |||
| * Proxy method to access BigRational's protected constructor from sibling classes. | |||
| * | |||
| * @internal | |||
| * @psalm-pure | |||
| */ | |||
| final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational | |||
| { | |||
| return new BigRational($numerator, $denominator, $checkDenominator); | |||
| } | |||
| /** | |||
| * Returns the minimum of the given values. | |||
| * | |||
| * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible | |||
| * to an instance of the class this method is called on. | |||
| * | |||
| * @throws \InvalidArgumentException If no values are given. | |||
| * @throws MathException If an argument is not valid. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| final public static function min(BigNumber|int|float|string ...$values) : static | |||
| { | |||
| $min = null; | |||
| foreach ($values as $value) { | |||
| $value = static::of($value); | |||
| if ($min === null || $value->isLessThan($min)) { | |||
| $min = $value; | |||
| } | |||
| } | |||
| if ($min === null) { | |||
| throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); | |||
| } | |||
| return $min; | |||
| } | |||
| /** | |||
| * Returns the maximum of the given values. | |||
| * | |||
| * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible | |||
| * to an instance of the class this method is called on. | |||
| * | |||
| * @throws \InvalidArgumentException If no values are given. | |||
| * @throws MathException If an argument is not valid. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| final public static function max(BigNumber|int|float|string ...$values) : static | |||
| { | |||
| $max = null; | |||
| foreach ($values as $value) { | |||
| $value = static::of($value); | |||
| if ($max === null || $value->isGreaterThan($max)) { | |||
| $max = $value; | |||
| } | |||
| } | |||
| if ($max === null) { | |||
| throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); | |||
| } | |||
| return $max; | |||
| } | |||
| /** | |||
| * Returns the sum of the given values. | |||
| * | |||
| * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible | |||
| * to an instance of the class this method is called on. | |||
| * | |||
| * @throws \InvalidArgumentException If no values are given. | |||
| * @throws MathException If an argument is not valid. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| final public static function sum(BigNumber|int|float|string ...$values) : static | |||
| { | |||
| /** @var static|null $sum */ | |||
| $sum = null; | |||
| foreach ($values as $value) { | |||
| $value = static::of($value); | |||
| $sum = $sum === null ? $value : self::add($sum, $value); | |||
| } | |||
| if ($sum === null) { | |||
| throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); | |||
| } | |||
| return $sum; | |||
| } | |||
| /** | |||
| * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. | |||
| * | |||
| * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to | |||
| * concrete classes the responsibility to perform the addition themselves or delegate it to the given number, | |||
| * depending on their ability to perform the operation. This will also require a version bump because we're | |||
| * potentially breaking custom BigNumber implementations (if any...) | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| private static function add(BigNumber $a, BigNumber $b) : BigNumber | |||
| { | |||
| if ($a instanceof BigRational) { | |||
| return $a->plus($b); | |||
| } | |||
| if ($b instanceof BigRational) { | |||
| return $b->plus($a); | |||
| } | |||
| if ($a instanceof BigDecimal) { | |||
| return $a->plus($b); | |||
| } | |||
| if ($b instanceof BigDecimal) { | |||
| return $b->plus($a); | |||
| } | |||
| /** @var BigInteger $a */ | |||
| return $a->plus($b); | |||
| } | |||
| /** | |||
| * Removes optional leading zeros and applies sign. | |||
| * | |||
| * @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'. | |||
| * @param string $number The number, validated as a non-empty string of digits. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| private static function cleanUp(string|null $sign, string $number) : string | |||
| { | |||
| $number = \ltrim($number, '0'); | |||
| if ($number === '') { | |||
| return '0'; | |||
| } | |||
| return $sign === '-' ? '-' . $number : $number; | |||
| } | |||
| /** | |||
| * Checks if this number is equal to the given one. | |||
| */ | |||
| final public function isEqualTo(BigNumber|int|float|string $that) : bool | |||
| { | |||
| return $this->compareTo($that) === 0; | |||
| } | |||
| /** | |||
| * Checks if this number is strictly lower than the given one. | |||
| */ | |||
| final public function isLessThan(BigNumber|int|float|string $that) : bool | |||
| { | |||
| return $this->compareTo($that) < 0; | |||
| } | |||
| /** | |||
| * Checks if this number is lower than or equal to the given one. | |||
| */ | |||
| final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool | |||
| { | |||
| return $this->compareTo($that) <= 0; | |||
| } | |||
| /** | |||
| * Checks if this number is strictly greater than the given one. | |||
| */ | |||
| final public function isGreaterThan(BigNumber|int|float|string $that) : bool | |||
| { | |||
| return $this->compareTo($that) > 0; | |||
| } | |||
| /** | |||
| * Checks if this number is greater than or equal to the given one. | |||
| */ | |||
| final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool | |||
| { | |||
| return $this->compareTo($that) >= 0; | |||
| } | |||
| /** | |||
| * Checks if this number equals zero. | |||
| */ | |||
| final public function isZero() : bool | |||
| { | |||
| return $this->getSign() === 0; | |||
| } | |||
| /** | |||
| * Checks if this number is strictly negative. | |||
| */ | |||
| final public function isNegative() : bool | |||
| { | |||
| return $this->getSign() < 0; | |||
| } | |||
| /** | |||
| * Checks if this number is negative or zero. | |||
| */ | |||
| final public function isNegativeOrZero() : bool | |||
| { | |||
| return $this->getSign() <= 0; | |||
| } | |||
| /** | |||
| * Checks if this number is strictly positive. | |||
| */ | |||
| final public function isPositive() : bool | |||
| { | |||
| return $this->getSign() > 0; | |||
| } | |||
| /** | |||
| * Checks if this number is positive or zero. | |||
| */ | |||
| final public function isPositiveOrZero() : bool | |||
| { | |||
| return $this->getSign() >= 0; | |||
| } | |||
| /** | |||
| * Returns the sign of this number. | |||
| * | |||
| * @psalm-return -1|0|1 | |||
| * | |||
| * @return int -1 if the number is negative, 0 if zero, 1 if positive. | |||
| */ | |||
| abstract public function getSign() : int; | |||
| /** | |||
| * Compares this number to the given one. | |||
| * | |||
| * @psalm-return -1|0|1 | |||
| * | |||
| * @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`. | |||
| * | |||
| * @throws MathException If the number is not valid. | |||
| */ | |||
| abstract public function compareTo(BigNumber|int|float|string $that) : int; | |||
| /** | |||
| * Converts this number to a BigInteger. | |||
| * | |||
| * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. | |||
| */ | |||
| abstract public function toBigInteger() : BigInteger; | |||
| /** | |||
| * Converts this number to a BigDecimal. | |||
| * | |||
| * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. | |||
| */ | |||
| abstract public function toBigDecimal() : BigDecimal; | |||
| /** | |||
| * Converts this number to a BigRational. | |||
| */ | |||
| abstract public function toBigRational() : BigRational; | |||
| /** | |||
| * Converts this number to a BigDecimal with the given scale, using rounding if necessary. | |||
| * | |||
| * @param int $scale The scale of the resulting `BigDecimal`. | |||
| * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. | |||
| * | |||
| * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding. | |||
| * This only applies when RoundingMode::UNNECESSARY is used. | |||
| */ | |||
| abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal; | |||
| /** | |||
| * Returns the exact value of this number as a native integer. | |||
| * | |||
| * If this number cannot be converted to a native integer without losing precision, an exception is thrown. | |||
| * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit. | |||
| * | |||
| * @throws MathException If this number cannot be exactly converted to a native integer. | |||
| */ | |||
| abstract public function toInt() : int; | |||
| /** | |||
| * Returns an approximation of this number as a floating-point value. | |||
| * | |||
| * Note that this method can discard information as the precision of a floating-point value | |||
| * is inherently limited. | |||
| * | |||
| * If the number is greater than the largest representable floating point number, positive infinity is returned. | |||
| * If the number is less than the smallest representable floating point number, negative infinity is returned. | |||
| */ | |||
| abstract public function toFloat() : float; | |||
| /** | |||
| * Returns a string representation of this number. | |||
| * | |||
| * The output of this method can be parsed by the `of()` factory method; | |||
| * this will yield an object equal to this one, without any information loss. | |||
| */ | |||
| abstract public function __toString() : string; | |||
| final public function jsonSerialize() : string | |||
| { | |||
| return $this->__toString(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,413 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math; | |||
| use Brick\Math\Exception\DivisionByZeroException; | |||
| use Brick\Math\Exception\MathException; | |||
| use Brick\Math\Exception\NumberFormatException; | |||
| use Brick\Math\Exception\RoundingNecessaryException; | |||
| /** | |||
| * An arbitrarily large rational number. | |||
| * | |||
| * This class is immutable. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| final class BigRational extends BigNumber | |||
| { | |||
| /** | |||
| * The numerator. | |||
| */ | |||
| private readonly BigInteger $numerator; | |||
| /** | |||
| * The denominator. Always strictly positive. | |||
| */ | |||
| private readonly BigInteger $denominator; | |||
| /** | |||
| * Protected constructor. Use a factory method to obtain an instance. | |||
| * | |||
| * @param BigInteger $numerator The numerator. | |||
| * @param BigInteger $denominator The denominator. | |||
| * @param bool $checkDenominator Whether to check the denominator for negative and zero. | |||
| * | |||
| * @throws DivisionByZeroException If the denominator is zero. | |||
| */ | |||
| protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) | |||
| { | |||
| if ($checkDenominator) { | |||
| if ($denominator->isZero()) { | |||
| throw DivisionByZeroException::denominatorMustNotBeZero(); | |||
| } | |||
| if ($denominator->isNegative()) { | |||
| $numerator = $numerator->negated(); | |||
| $denominator = $denominator->negated(); | |||
| } | |||
| } | |||
| $this->numerator = $numerator; | |||
| $this->denominator = $denominator; | |||
| } | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| protected static function from(BigNumber $number): static | |||
| { | |||
| return $number->toBigRational(); | |||
| } | |||
| /** | |||
| * Creates a BigRational out of a numerator and a denominator. | |||
| * | |||
| * If the denominator is negative, the signs of both the numerator and the denominator | |||
| * will be inverted to ensure that the denominator is always positive. | |||
| * | |||
| * @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger. | |||
| * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger. | |||
| * | |||
| * @throws NumberFormatException If an argument does not represent a valid number. | |||
| * @throws RoundingNecessaryException If an argument represents a non-integer number. | |||
| * @throws DivisionByZeroException If the denominator is zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function nd( | |||
| BigNumber|int|float|string $numerator, | |||
| BigNumber|int|float|string $denominator, | |||
| ) : BigRational { | |||
| $numerator = BigInteger::of($numerator); | |||
| $denominator = BigInteger::of($denominator); | |||
| return new BigRational($numerator, $denominator, true); | |||
| } | |||
| /** | |||
| * Returns a BigRational representing zero. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function zero() : BigRational | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigRational|null $zero | |||
| */ | |||
| static $zero; | |||
| if ($zero === null) { | |||
| $zero = new BigRational(BigInteger::zero(), BigInteger::one(), false); | |||
| } | |||
| return $zero; | |||
| } | |||
| /** | |||
| * Returns a BigRational representing one. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function one() : BigRational | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigRational|null $one | |||
| */ | |||
| static $one; | |||
| if ($one === null) { | |||
| $one = new BigRational(BigInteger::one(), BigInteger::one(), false); | |||
| } | |||
| return $one; | |||
| } | |||
| /** | |||
| * Returns a BigRational representing ten. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function ten() : BigRational | |||
| { | |||
| /** | |||
| * @psalm-suppress ImpureStaticVariable | |||
| * @var BigRational|null $ten | |||
| */ | |||
| static $ten; | |||
| if ($ten === null) { | |||
| $ten = new BigRational(BigInteger::ten(), BigInteger::one(), false); | |||
| } | |||
| return $ten; | |||
| } | |||
| public function getNumerator() : BigInteger | |||
| { | |||
| return $this->numerator; | |||
| } | |||
| public function getDenominator() : BigInteger | |||
| { | |||
| return $this->denominator; | |||
| } | |||
| /** | |||
| * Returns the quotient of the division of the numerator by the denominator. | |||
| */ | |||
| public function quotient() : BigInteger | |||
| { | |||
| return $this->numerator->quotient($this->denominator); | |||
| } | |||
| /** | |||
| * Returns the remainder of the division of the numerator by the denominator. | |||
| */ | |||
| public function remainder() : BigInteger | |||
| { | |||
| return $this->numerator->remainder($this->denominator); | |||
| } | |||
| /** | |||
| * Returns the quotient and remainder of the division of the numerator by the denominator. | |||
| * | |||
| * @return BigInteger[] | |||
| * | |||
| * @psalm-return array{BigInteger, BigInteger} | |||
| */ | |||
| public function quotientAndRemainder() : array | |||
| { | |||
| return $this->numerator->quotientAndRemainder($this->denominator); | |||
| } | |||
| /** | |||
| * Returns the sum of this number and the given one. | |||
| * | |||
| * @param BigNumber|int|float|string $that The number to add. | |||
| * | |||
| * @throws MathException If the number is not valid. | |||
| */ | |||
| public function plus(BigNumber|int|float|string $that) : BigRational | |||
| { | |||
| $that = BigRational::of($that); | |||
| $numerator = $this->numerator->multipliedBy($that->denominator); | |||
| $numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator)); | |||
| $denominator = $this->denominator->multipliedBy($that->denominator); | |||
| return new BigRational($numerator, $denominator, false); | |||
| } | |||
| /** | |||
| * Returns the difference of this number and the given one. | |||
| * | |||
| * @param BigNumber|int|float|string $that The number to subtract. | |||
| * | |||
| * @throws MathException If the number is not valid. | |||
| */ | |||
| public function minus(BigNumber|int|float|string $that) : BigRational | |||
| { | |||
| $that = BigRational::of($that); | |||
| $numerator = $this->numerator->multipliedBy($that->denominator); | |||
| $numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator)); | |||
| $denominator = $this->denominator->multipliedBy($that->denominator); | |||
| return new BigRational($numerator, $denominator, false); | |||
| } | |||
| /** | |||
| * Returns the product of this number and the given one. | |||
| * | |||
| * @param BigNumber|int|float|string $that The multiplier. | |||
| * | |||
| * @throws MathException If the multiplier is not a valid number. | |||
| */ | |||
| public function multipliedBy(BigNumber|int|float|string $that) : BigRational | |||
| { | |||
| $that = BigRational::of($that); | |||
| $numerator = $this->numerator->multipliedBy($that->numerator); | |||
| $denominator = $this->denominator->multipliedBy($that->denominator); | |||
| return new BigRational($numerator, $denominator, false); | |||
| } | |||
| /** | |||
| * Returns the result of the division of this number by the given one. | |||
| * | |||
| * @param BigNumber|int|float|string $that The divisor. | |||
| * | |||
| * @throws MathException If the divisor is not a valid number, or is zero. | |||
| */ | |||
| public function dividedBy(BigNumber|int|float|string $that) : BigRational | |||
| { | |||
| $that = BigRational::of($that); | |||
| $numerator = $this->numerator->multipliedBy($that->denominator); | |||
| $denominator = $this->denominator->multipliedBy($that->numerator); | |||
| return new BigRational($numerator, $denominator, true); | |||
| } | |||
| /** | |||
| * Returns this number exponentiated to the given value. | |||
| * | |||
| * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. | |||
| */ | |||
| public function power(int $exponent) : BigRational | |||
| { | |||
| if ($exponent === 0) { | |||
| $one = BigInteger::one(); | |||
| return new BigRational($one, $one, false); | |||
| } | |||
| if ($exponent === 1) { | |||
| return $this; | |||
| } | |||
| return new BigRational( | |||
| $this->numerator->power($exponent), | |||
| $this->denominator->power($exponent), | |||
| false | |||
| ); | |||
| } | |||
| /** | |||
| * Returns the reciprocal of this BigRational. | |||
| * | |||
| * The reciprocal has the numerator and denominator swapped. | |||
| * | |||
| * @throws DivisionByZeroException If the numerator is zero. | |||
| */ | |||
| public function reciprocal() : BigRational | |||
| { | |||
| return new BigRational($this->denominator, $this->numerator, true); | |||
| } | |||
| /** | |||
| * Returns the absolute value of this BigRational. | |||
| */ | |||
| public function abs() : BigRational | |||
| { | |||
| return new BigRational($this->numerator->abs(), $this->denominator, false); | |||
| } | |||
| /** | |||
| * Returns the negated value of this BigRational. | |||
| */ | |||
| public function negated() : BigRational | |||
| { | |||
| return new BigRational($this->numerator->negated(), $this->denominator, false); | |||
| } | |||
| /** | |||
| * Returns the simplified value of this BigRational. | |||
| */ | |||
| public function simplified() : BigRational | |||
| { | |||
| $gcd = $this->numerator->gcd($this->denominator); | |||
| $numerator = $this->numerator->quotient($gcd); | |||
| $denominator = $this->denominator->quotient($gcd); | |||
| return new BigRational($numerator, $denominator, false); | |||
| } | |||
| public function compareTo(BigNumber|int|float|string $that) : int | |||
| { | |||
| return $this->minus($that)->getSign(); | |||
| } | |||
| public function getSign() : int | |||
| { | |||
| return $this->numerator->getSign(); | |||
| } | |||
| public function toBigInteger() : BigInteger | |||
| { | |||
| $simplified = $this->simplified(); | |||
| if (! $simplified->denominator->isEqualTo(1)) { | |||
| throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.'); | |||
| } | |||
| return $simplified->numerator; | |||
| } | |||
| public function toBigDecimal() : BigDecimal | |||
| { | |||
| return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); | |||
| } | |||
| public function toBigRational() : BigRational | |||
| { | |||
| return $this; | |||
| } | |||
| public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal | |||
| { | |||
| return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); | |||
| } | |||
| public function toInt() : int | |||
| { | |||
| return $this->toBigInteger()->toInt(); | |||
| } | |||
| public function toFloat() : float | |||
| { | |||
| $simplified = $this->simplified(); | |||
| return $simplified->numerator->toFloat() / $simplified->denominator->toFloat(); | |||
| } | |||
| public function __toString() : string | |||
| { | |||
| $numerator = (string) $this->numerator; | |||
| $denominator = (string) $this->denominator; | |||
| if ($denominator === '1') { | |||
| return $numerator; | |||
| } | |||
| return $this->numerator . '/' . $this->denominator; | |||
| } | |||
| /** | |||
| * This method is required for serializing the object and SHOULD NOT be accessed directly. | |||
| * | |||
| * @internal | |||
| * | |||
| * @return array{numerator: BigInteger, denominator: BigInteger} | |||
| */ | |||
| public function __serialize(): array | |||
| { | |||
| return ['numerator' => $this->numerator, 'denominator' => $this->denominator]; | |||
| } | |||
| /** | |||
| * This method is only here to allow unserializing the object and cannot be accessed directly. | |||
| * | |||
| * @internal | |||
| * @psalm-suppress RedundantPropertyInitializationCheck | |||
| * | |||
| * @param array{numerator: BigInteger, denominator: BigInteger} $data | |||
| * | |||
| * @throws \LogicException | |||
| */ | |||
| public function __unserialize(array $data): void | |||
| { | |||
| if (isset($this->numerator)) { | |||
| throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); | |||
| } | |||
| $this->numerator = $data['numerator']; | |||
| $this->denominator = $data['denominator']; | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| /** | |||
| * Exception thrown when a division by zero occurs. | |||
| */ | |||
| class DivisionByZeroException extends MathException | |||
| { | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| public static function divisionByZero() : DivisionByZeroException | |||
| { | |||
| return new self('Division by zero.'); | |||
| } | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| public static function modulusMustNotBeZero() : DivisionByZeroException | |||
| { | |||
| return new self('The modulus must not be zero.'); | |||
| } | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| public static function denominatorMustNotBeZero() : DivisionByZeroException | |||
| { | |||
| return new self('The denominator of a rational number cannot be zero.'); | |||
| } | |||
| } | |||
| @@ -0,0 +1,23 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| use Brick\Math\BigInteger; | |||
| /** | |||
| * Exception thrown when an integer overflow occurs. | |||
| */ | |||
| class IntegerOverflowException extends MathException | |||
| { | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| public static function toIntOverflow(BigInteger $value) : IntegerOverflowException | |||
| { | |||
| $message = '%s is out of range %d to %d and cannot be represented as an integer.'; | |||
| return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| /** | |||
| * Base class for all math exceptions. | |||
| */ | |||
| class MathException extends \Exception | |||
| { | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| /** | |||
| * Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number. | |||
| */ | |||
| class NegativeNumberException extends MathException | |||
| { | |||
| } | |||
| @@ -0,0 +1,41 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| /** | |||
| * Exception thrown when attempting to create a number from a string with an invalid format. | |||
| */ | |||
| class NumberFormatException extends MathException | |||
| { | |||
| public static function invalidFormat(string $value) : self | |||
| { | |||
| return new self(\sprintf( | |||
| 'The given value "%s" does not represent a valid number.', | |||
| $value, | |||
| )); | |||
| } | |||
| /** | |||
| * @param string $char The failing character. | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function charNotInAlphabet(string $char) : self | |||
| { | |||
| $ord = \ord($char); | |||
| if ($ord < 32 || $ord > 126) { | |||
| $char = \strtoupper(\dechex($ord)); | |||
| if ($ord < 10) { | |||
| $char = '0' . $char; | |||
| } | |||
| } else { | |||
| $char = '"' . $char . '"'; | |||
| } | |||
| return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Exception; | |||
| /** | |||
| * Exception thrown when a number cannot be represented at the requested scale without rounding. | |||
| */ | |||
| class RoundingNecessaryException extends MathException | |||
| { | |||
| /** | |||
| * @psalm-pure | |||
| */ | |||
| public static function roundingNecessary() : RoundingNecessaryException | |||
| { | |||
| return new self('Rounding is necessary to represent the result of the operation at this scale.'); | |||
| } | |||
| } | |||
| @@ -0,0 +1,668 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Internal; | |||
| use Brick\Math\Exception\RoundingNecessaryException; | |||
| use Brick\Math\RoundingMode; | |||
| /** | |||
| * Performs basic operations on arbitrary size integers. | |||
| * | |||
| * Unless otherwise specified, all parameters must be validated as non-empty strings of digits, | |||
| * without leading zero, and with an optional leading minus sign if the number is not zero. | |||
| * | |||
| * Any other parameter format will lead to undefined behaviour. | |||
| * All methods must return strings respecting this format, unless specified otherwise. | |||
| * | |||
| * @internal | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| abstract class Calculator | |||
| { | |||
| /** | |||
| * The maximum exponent value allowed for the pow() method. | |||
| */ | |||
| public const MAX_POWER = 1_000_000; | |||
| /** | |||
| * The alphabet for converting from and to base 2 to 36, lowercase. | |||
| */ | |||
| public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; | |||
| /** | |||
| * The Calculator instance in use. | |||
| */ | |||
| private static ?Calculator $instance = null; | |||
| /** | |||
| * Sets the Calculator instance to use. | |||
| * | |||
| * An instance is typically set only in unit tests: the autodetect is usually the best option. | |||
| * | |||
| * @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect. | |||
| */ | |||
| final public static function set(?Calculator $calculator) : void | |||
| { | |||
| self::$instance = $calculator; | |||
| } | |||
| /** | |||
| * Returns the Calculator instance to use. | |||
| * | |||
| * If none has been explicitly set, the fastest available implementation will be returned. | |||
| * | |||
| * @psalm-pure | |||
| * @psalm-suppress ImpureStaticProperty | |||
| */ | |||
| final public static function get() : Calculator | |||
| { | |||
| if (self::$instance === null) { | |||
| /** @psalm-suppress ImpureMethodCall */ | |||
| self::$instance = self::detect(); | |||
| } | |||
| return self::$instance; | |||
| } | |||
| /** | |||
| * Returns the fastest available Calculator implementation. | |||
| * | |||
| * @codeCoverageIgnore | |||
| */ | |||
| private static function detect() : Calculator | |||
| { | |||
| if (\extension_loaded('gmp')) { | |||
| return new Calculator\GmpCalculator(); | |||
| } | |||
| if (\extension_loaded('bcmath')) { | |||
| return new Calculator\BcMathCalculator(); | |||
| } | |||
| return new Calculator\NativeCalculator(); | |||
| } | |||
| /** | |||
| * Extracts the sign & digits of the operands. | |||
| * | |||
| * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits. | |||
| */ | |||
| final protected function init(string $a, string $b) : array | |||
| { | |||
| return [ | |||
| $aNeg = ($a[0] === '-'), | |||
| $bNeg = ($b[0] === '-'), | |||
| $aNeg ? \substr($a, 1) : $a, | |||
| $bNeg ? \substr($b, 1) : $b, | |||
| ]; | |||
| } | |||
| /** | |||
| * Returns the absolute value of a number. | |||
| */ | |||
| final public function abs(string $n) : string | |||
| { | |||
| return ($n[0] === '-') ? \substr($n, 1) : $n; | |||
| } | |||
| /** | |||
| * Negates a number. | |||
| */ | |||
| final public function neg(string $n) : string | |||
| { | |||
| if ($n === '0') { | |||
| return '0'; | |||
| } | |||
| if ($n[0] === '-') { | |||
| return \substr($n, 1); | |||
| } | |||
| return '-' . $n; | |||
| } | |||
| /** | |||
| * Compares two numbers. | |||
| * | |||
| * @psalm-return -1|0|1 | |||
| * | |||
| * @return int -1 if the first number is less than, 0 if equal to, 1 if greater than the second number. | |||
| */ | |||
| final public function cmp(string $a, string $b) : int | |||
| { | |||
| [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); | |||
| if ($aNeg && ! $bNeg) { | |||
| return -1; | |||
| } | |||
| if ($bNeg && ! $aNeg) { | |||
| return 1; | |||
| } | |||
| $aLen = \strlen($aDig); | |||
| $bLen = \strlen($bDig); | |||
| if ($aLen < $bLen) { | |||
| $result = -1; | |||
| } elseif ($aLen > $bLen) { | |||
| $result = 1; | |||
| } else { | |||
| $result = $aDig <=> $bDig; | |||
| } | |||
| return $aNeg ? -$result : $result; | |||
| } | |||
| /** | |||
| * Adds two numbers. | |||
| */ | |||
| abstract public function add(string $a, string $b) : string; | |||
| /** | |||
| * Subtracts two numbers. | |||
| */ | |||
| abstract public function sub(string $a, string $b) : string; | |||
| /** | |||
| * Multiplies two numbers. | |||
| */ | |||
| abstract public function mul(string $a, string $b) : string; | |||
| /** | |||
| * Returns the quotient of the division of two numbers. | |||
| * | |||
| * @param string $a The dividend. | |||
| * @param string $b The divisor, must not be zero. | |||
| * | |||
| * @return string The quotient. | |||
| */ | |||
| abstract public function divQ(string $a, string $b) : string; | |||
| /** | |||
| * Returns the remainder of the division of two numbers. | |||
| * | |||
| * @param string $a The dividend. | |||
| * @param string $b The divisor, must not be zero. | |||
| * | |||
| * @return string The remainder. | |||
| */ | |||
| abstract public function divR(string $a, string $b) : string; | |||
| /** | |||
| * Returns the quotient and remainder of the division of two numbers. | |||
| * | |||
| * @param string $a The dividend. | |||
| * @param string $b The divisor, must not be zero. | |||
| * | |||
| * @return array{string, string} An array containing the quotient and remainder. | |||
| */ | |||
| abstract public function divQR(string $a, string $b) : array; | |||
| /** | |||
| * Exponentiates a number. | |||
| * | |||
| * @param string $a The base number. | |||
| * @param int $e The exponent, validated as an integer between 0 and MAX_POWER. | |||
| * | |||
| * @return string The power. | |||
| */ | |||
| abstract public function pow(string $a, int $e) : string; | |||
| /** | |||
| * @param string $b The modulus; must not be zero. | |||
| */ | |||
| public function mod(string $a, string $b) : string | |||
| { | |||
| return $this->divR($this->add($this->divR($a, $b), $b), $b); | |||
| } | |||
| /** | |||
| * Returns the modular multiplicative inverse of $x modulo $m. | |||
| * | |||
| * If $x has no multiplicative inverse mod m, this method must return null. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library has built-in support. | |||
| * | |||
| * @param string $m The modulus; must not be negative or zero. | |||
| */ | |||
| public function modInverse(string $x, string $m) : ?string | |||
| { | |||
| if ($m === '1') { | |||
| return '0'; | |||
| } | |||
| $modVal = $x; | |||
| if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) { | |||
| $modVal = $this->mod($x, $m); | |||
| } | |||
| [$g, $x] = $this->gcdExtended($modVal, $m); | |||
| if ($g !== '1') { | |||
| return null; | |||
| } | |||
| return $this->mod($this->add($this->mod($x, $m), $m), $m); | |||
| } | |||
| /** | |||
| * Raises a number into power with modulo. | |||
| * | |||
| * @param string $base The base number; must be positive or zero. | |||
| * @param string $exp The exponent; must be positive or zero. | |||
| * @param string $mod The modulus; must be strictly positive. | |||
| */ | |||
| abstract public function modPow(string $base, string $exp, string $mod) : string; | |||
| /** | |||
| * Returns the greatest common divisor of the two numbers. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for GCD calculations. | |||
| * | |||
| * @return string The GCD, always positive, or zero if both arguments are zero. | |||
| */ | |||
| public function gcd(string $a, string $b) : string | |||
| { | |||
| if ($a === '0') { | |||
| return $this->abs($b); | |||
| } | |||
| if ($b === '0') { | |||
| return $this->abs($a); | |||
| } | |||
| return $this->gcd($b, $this->divR($a, $b)); | |||
| } | |||
| /** | |||
| * @return array{string, string, string} GCD, X, Y | |||
| */ | |||
| private function gcdExtended(string $a, string $b) : array | |||
| { | |||
| if ($a === '0') { | |||
| return [$b, '0', '1']; | |||
| } | |||
| [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a); | |||
| $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1)); | |||
| $y = $x1; | |||
| return [$gcd, $x, $y]; | |||
| } | |||
| /** | |||
| * Returns the square root of the given number, rounded down. | |||
| * | |||
| * The result is the largest x such that x² ≤ n. | |||
| * The input MUST NOT be negative. | |||
| */ | |||
| abstract public function sqrt(string $n) : string; | |||
| /** | |||
| * Converts a number from an arbitrary base. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for base conversion. | |||
| * | |||
| * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base. | |||
| * @param int $base The base of the number, validated from 2 to 36. | |||
| * | |||
| * @return string The converted number, following the Calculator conventions. | |||
| */ | |||
| public function fromBase(string $number, int $base) : string | |||
| { | |||
| return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base); | |||
| } | |||
| /** | |||
| * Converts a number to an arbitrary base. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for base conversion. | |||
| * | |||
| * @param string $number The number to convert, following the Calculator conventions. | |||
| * @param int $base The base to convert to, validated from 2 to 36. | |||
| * | |||
| * @return string The converted number, lowercase. | |||
| */ | |||
| public function toBase(string $number, int $base) : string | |||
| { | |||
| $negative = ($number[0] === '-'); | |||
| if ($negative) { | |||
| $number = \substr($number, 1); | |||
| } | |||
| $number = $this->toArbitraryBase($number, self::ALPHABET, $base); | |||
| if ($negative) { | |||
| return '-' . $number; | |||
| } | |||
| return $number; | |||
| } | |||
| /** | |||
| * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10. | |||
| * | |||
| * @param string $number The number to convert, validated as a non-empty string, | |||
| * containing only chars in the given alphabet/base. | |||
| * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. | |||
| * @param int $base The base of the number, validated from 2 to alphabet length. | |||
| * | |||
| * @return string The number in base 10, following the Calculator conventions. | |||
| */ | |||
| final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string | |||
| { | |||
| // remove leading "zeros" | |||
| $number = \ltrim($number, $alphabet[0]); | |||
| if ($number === '') { | |||
| return '0'; | |||
| } | |||
| // optimize for "one" | |||
| if ($number === $alphabet[1]) { | |||
| return '1'; | |||
| } | |||
| $result = '0'; | |||
| $power = '1'; | |||
| $base = (string) $base; | |||
| for ($i = \strlen($number) - 1; $i >= 0; $i--) { | |||
| $index = \strpos($alphabet, $number[$i]); | |||
| if ($index !== 0) { | |||
| $result = $this->add($result, ($index === 1) | |||
| ? $power | |||
| : $this->mul($power, (string) $index) | |||
| ); | |||
| } | |||
| if ($i !== 0) { | |||
| $power = $this->mul($power, $base); | |||
| } | |||
| } | |||
| return $result; | |||
| } | |||
| /** | |||
| * Converts a non-negative number to an arbitrary base using a custom alphabet. | |||
| * | |||
| * @param string $number The number to convert, positive or zero, following the Calculator conventions. | |||
| * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. | |||
| * @param int $base The base to convert to, validated from 2 to alphabet length. | |||
| * | |||
| * @return string The converted number in the given alphabet. | |||
| */ | |||
| final public function toArbitraryBase(string $number, string $alphabet, int $base) : string | |||
| { | |||
| if ($number === '0') { | |||
| return $alphabet[0]; | |||
| } | |||
| $base = (string) $base; | |||
| $result = ''; | |||
| while ($number !== '0') { | |||
| [$number, $remainder] = $this->divQR($number, $base); | |||
| $remainder = (int) $remainder; | |||
| $result .= $alphabet[$remainder]; | |||
| } | |||
| return \strrev($result); | |||
| } | |||
| /** | |||
| * Performs a rounded division. | |||
| * | |||
| * Rounding is performed when the remainder of the division is not zero. | |||
| * | |||
| * @param string $a The dividend. | |||
| * @param string $b The divisor, must not be zero. | |||
| * @param RoundingMode $roundingMode The rounding mode. | |||
| * | |||
| * @throws \InvalidArgumentException If the rounding mode is invalid. | |||
| * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary. | |||
| * | |||
| * @psalm-suppress ImpureFunctionCall | |||
| */ | |||
| final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string | |||
| { | |||
| [$quotient, $remainder] = $this->divQR($a, $b); | |||
| $hasDiscardedFraction = ($remainder !== '0'); | |||
| $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-'); | |||
| $discardedFractionSign = function() use ($remainder, $b) : int { | |||
| $r = $this->abs($this->mul($remainder, '2')); | |||
| $b = $this->abs($b); | |||
| return $this->cmp($r, $b); | |||
| }; | |||
| $increment = false; | |||
| switch ($roundingMode) { | |||
| case RoundingMode::UNNECESSARY: | |||
| if ($hasDiscardedFraction) { | |||
| throw RoundingNecessaryException::roundingNecessary(); | |||
| } | |||
| break; | |||
| case RoundingMode::UP: | |||
| $increment = $hasDiscardedFraction; | |||
| break; | |||
| case RoundingMode::DOWN: | |||
| break; | |||
| case RoundingMode::CEILING: | |||
| $increment = $hasDiscardedFraction && $isPositiveOrZero; | |||
| break; | |||
| case RoundingMode::FLOOR: | |||
| $increment = $hasDiscardedFraction && ! $isPositiveOrZero; | |||
| break; | |||
| case RoundingMode::HALF_UP: | |||
| $increment = $discardedFractionSign() >= 0; | |||
| break; | |||
| case RoundingMode::HALF_DOWN: | |||
| $increment = $discardedFractionSign() > 0; | |||
| break; | |||
| case RoundingMode::HALF_CEILING: | |||
| $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0; | |||
| break; | |||
| case RoundingMode::HALF_FLOOR: | |||
| $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; | |||
| break; | |||
| case RoundingMode::HALF_EVEN: | |||
| $lastDigit = (int) $quotient[-1]; | |||
| $lastDigitIsEven = ($lastDigit % 2 === 0); | |||
| $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; | |||
| break; | |||
| default: | |||
| throw new \InvalidArgumentException('Invalid rounding mode.'); | |||
| } | |||
| if ($increment) { | |||
| return $this->add($quotient, $isPositiveOrZero ? '1' : '-1'); | |||
| } | |||
| return $quotient; | |||
| } | |||
| /** | |||
| * Calculates bitwise AND of two numbers. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for bitwise operations. | |||
| */ | |||
| public function and(string $a, string $b) : string | |||
| { | |||
| return $this->bitwise('and', $a, $b); | |||
| } | |||
| /** | |||
| * Calculates bitwise OR of two numbers. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for bitwise operations. | |||
| */ | |||
| public function or(string $a, string $b) : string | |||
| { | |||
| return $this->bitwise('or', $a, $b); | |||
| } | |||
| /** | |||
| * Calculates bitwise XOR of two numbers. | |||
| * | |||
| * This method can be overridden by the concrete implementation if the underlying library | |||
| * has built-in support for bitwise operations. | |||
| */ | |||
| public function xor(string $a, string $b) : string | |||
| { | |||
| return $this->bitwise('xor', $a, $b); | |||
| } | |||
| /** | |||
| * Performs a bitwise operation on a decimal number. | |||
| * | |||
| * @param 'and'|'or'|'xor' $operator The operator to use. | |||
| * @param string $a The left operand. | |||
| * @param string $b The right operand. | |||
| */ | |||
| private function bitwise(string $operator, string $a, string $b) : string | |||
| { | |||
| [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); | |||
| $aBin = $this->toBinary($aDig); | |||
| $bBin = $this->toBinary($bDig); | |||
| $aLen = \strlen($aBin); | |||
| $bLen = \strlen($bBin); | |||
| if ($aLen > $bLen) { | |||
| $bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin; | |||
| } elseif ($bLen > $aLen) { | |||
| $aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin; | |||
| } | |||
| if ($aNeg) { | |||
| $aBin = $this->twosComplement($aBin); | |||
| } | |||
| if ($bNeg) { | |||
| $bBin = $this->twosComplement($bBin); | |||
| } | |||
| $value = match ($operator) { | |||
| 'and' => $aBin & $bBin, | |||
| 'or' => $aBin | $bBin, | |||
| 'xor' => $aBin ^ $bBin, | |||
| }; | |||
| $negative = match ($operator) { | |||
| 'and' => $aNeg and $bNeg, | |||
| 'or' => $aNeg or $bNeg, | |||
| 'xor' => $aNeg xor $bNeg, | |||
| }; | |||
| if ($negative) { | |||
| $value = $this->twosComplement($value); | |||
| } | |||
| $result = $this->toDecimal($value); | |||
| return $negative ? $this->neg($result) : $result; | |||
| } | |||
| /** | |||
| * @param string $number A positive, binary number. | |||
| */ | |||
| private function twosComplement(string $number) : string | |||
| { | |||
| $xor = \str_repeat("\xff", \strlen($number)); | |||
| $number ^= $xor; | |||
| for ($i = \strlen($number) - 1; $i >= 0; $i--) { | |||
| $byte = \ord($number[$i]); | |||
| if (++$byte !== 256) { | |||
| $number[$i] = \chr($byte); | |||
| break; | |||
| } | |||
| $number[$i] = "\x00"; | |||
| if ($i === 0) { | |||
| $number = "\x01" . $number; | |||
| } | |||
| } | |||
| return $number; | |||
| } | |||
| /** | |||
| * Converts a decimal number to a binary string. | |||
| * | |||
| * @param string $number The number to convert, positive or zero, only digits. | |||
| */ | |||
| private function toBinary(string $number) : string | |||
| { | |||
| $result = ''; | |||
| while ($number !== '0') { | |||
| [$number, $remainder] = $this->divQR($number, '256'); | |||
| $result .= \chr((int) $remainder); | |||
| } | |||
| return \strrev($result); | |||
| } | |||
| /** | |||
| * Returns the positive decimal representation of a binary number. | |||
| * | |||
| * @param string $bytes The bytes representing the number. | |||
| */ | |||
| private function toDecimal(string $bytes) : string | |||
| { | |||
| $result = '0'; | |||
| $power = '1'; | |||
| for ($i = \strlen($bytes) - 1; $i >= 0; $i--) { | |||
| $index = \ord($bytes[$i]); | |||
| if ($index !== 0) { | |||
| $result = $this->add($result, ($index === 1) | |||
| ? $power | |||
| : $this->mul($power, (string) $index) | |||
| ); | |||
| } | |||
| if ($i !== 0) { | |||
| $power = $this->mul($power, '256'); | |||
| } | |||
| } | |||
| return $result; | |||
| } | |||
| } | |||
| @@ -0,0 +1,65 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Internal\Calculator; | |||
| use Brick\Math\Internal\Calculator; | |||
| /** | |||
| * Calculator implementation built around the bcmath library. | |||
| * | |||
| * @internal | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class BcMathCalculator extends Calculator | |||
| { | |||
| public function add(string $a, string $b) : string | |||
| { | |||
| return \bcadd($a, $b, 0); | |||
| } | |||
| public function sub(string $a, string $b) : string | |||
| { | |||
| return \bcsub($a, $b, 0); | |||
| } | |||
| public function mul(string $a, string $b) : string | |||
| { | |||
| return \bcmul($a, $b, 0); | |||
| } | |||
| public function divQ(string $a, string $b) : string | |||
| { | |||
| return \bcdiv($a, $b, 0); | |||
| } | |||
| public function divR(string $a, string $b) : string | |||
| { | |||
| return \bcmod($a, $b, 0); | |||
| } | |||
| public function divQR(string $a, string $b) : array | |||
| { | |||
| $q = \bcdiv($a, $b, 0); | |||
| $r = \bcmod($a, $b, 0); | |||
| return [$q, $r]; | |||
| } | |||
| public function pow(string $a, int $e) : string | |||
| { | |||
| return \bcpow($a, (string) $e, 0); | |||
| } | |||
| public function modPow(string $base, string $exp, string $mod) : string | |||
| { | |||
| return \bcpowmod($base, $exp, $mod, 0); | |||
| } | |||
| public function sqrt(string $n) : string | |||
| { | |||
| return \bcsqrt($n, 0); | |||
| } | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Internal\Calculator; | |||
| use Brick\Math\Internal\Calculator; | |||
| /** | |||
| * Calculator implementation built around the GMP library. | |||
| * | |||
| * @internal | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class GmpCalculator extends Calculator | |||
| { | |||
| public function add(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_add($a, $b)); | |||
| } | |||
| public function sub(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_sub($a, $b)); | |||
| } | |||
| public function mul(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_mul($a, $b)); | |||
| } | |||
| public function divQ(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_div_q($a, $b)); | |||
| } | |||
| public function divR(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_div_r($a, $b)); | |||
| } | |||
| public function divQR(string $a, string $b) : array | |||
| { | |||
| [$q, $r] = \gmp_div_qr($a, $b); | |||
| return [ | |||
| \gmp_strval($q), | |||
| \gmp_strval($r) | |||
| ]; | |||
| } | |||
| public function pow(string $a, int $e) : string | |||
| { | |||
| return \gmp_strval(\gmp_pow($a, $e)); | |||
| } | |||
| public function modInverse(string $x, string $m) : ?string | |||
| { | |||
| $result = \gmp_invert($x, $m); | |||
| if ($result === false) { | |||
| return null; | |||
| } | |||
| return \gmp_strval($result); | |||
| } | |||
| public function modPow(string $base, string $exp, string $mod) : string | |||
| { | |||
| return \gmp_strval(\gmp_powm($base, $exp, $mod)); | |||
| } | |||
| public function gcd(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_gcd($a, $b)); | |||
| } | |||
| public function fromBase(string $number, int $base) : string | |||
| { | |||
| return \gmp_strval(\gmp_init($number, $base)); | |||
| } | |||
| public function toBase(string $number, int $base) : string | |||
| { | |||
| return \gmp_strval($number, $base); | |||
| } | |||
| public function and(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_and($a, $b)); | |||
| } | |||
| public function or(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_or($a, $b)); | |||
| } | |||
| public function xor(string $a, string $b) : string | |||
| { | |||
| return \gmp_strval(\gmp_xor($a, $b)); | |||
| } | |||
| public function sqrt(string $n) : string | |||
| { | |||
| return \gmp_strval(\gmp_sqrt($n)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,572 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math\Internal\Calculator; | |||
| use Brick\Math\Internal\Calculator; | |||
| /** | |||
| * Calculator implementation using only native PHP code. | |||
| * | |||
| * @internal | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class NativeCalculator extends Calculator | |||
| { | |||
| /** | |||
| * The max number of digits the platform can natively add, subtract, multiply or divide without overflow. | |||
| * For multiplication, this represents the max sum of the lengths of both operands. | |||
| * | |||
| * In addition, it is assumed that an extra digit can hold a carry (1) without overflowing. | |||
| * Example: 32-bit: max number 1,999,999,999 (9 digits + carry) | |||
| * 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry) | |||
| */ | |||
| private readonly int $maxDigits; | |||
| /** | |||
| * @codeCoverageIgnore | |||
| */ | |||
| public function __construct() | |||
| { | |||
| $this->maxDigits = match (PHP_INT_SIZE) { | |||
| 4 => 9, | |||
| 8 => 18, | |||
| default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.') | |||
| }; | |||
| } | |||
| public function add(string $a, string $b) : string | |||
| { | |||
| /** | |||
| * @psalm-var numeric-string $a | |||
| * @psalm-var numeric-string $b | |||
| */ | |||
| $result = $a + $b; | |||
| if (is_int($result)) { | |||
| return (string) $result; | |||
| } | |||
| if ($a === '0') { | |||
| return $b; | |||
| } | |||
| if ($b === '0') { | |||
| return $a; | |||
| } | |||
| [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); | |||
| $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig); | |||
| if ($aNeg) { | |||
| $result = $this->neg($result); | |||
| } | |||
| return $result; | |||
| } | |||
| public function sub(string $a, string $b) : string | |||
| { | |||
| return $this->add($a, $this->neg($b)); | |||
| } | |||
| public function mul(string $a, string $b) : string | |||
| { | |||
| /** | |||
| * @psalm-var numeric-string $a | |||
| * @psalm-var numeric-string $b | |||
| */ | |||
| $result = $a * $b; | |||
| if (is_int($result)) { | |||
| return (string) $result; | |||
| } | |||
| if ($a === '0' || $b === '0') { | |||
| return '0'; | |||
| } | |||
| if ($a === '1') { | |||
| return $b; | |||
| } | |||
| if ($b === '1') { | |||
| return $a; | |||
| } | |||
| if ($a === '-1') { | |||
| return $this->neg($b); | |||
| } | |||
| if ($b === '-1') { | |||
| return $this->neg($a); | |||
| } | |||
| [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); | |||
| $result = $this->doMul($aDig, $bDig); | |||
| if ($aNeg !== $bNeg) { | |||
| $result = $this->neg($result); | |||
| } | |||
| return $result; | |||
| } | |||
| public function divQ(string $a, string $b) : string | |||
| { | |||
| return $this->divQR($a, $b)[0]; | |||
| } | |||
| public function divR(string $a, string $b): string | |||
| { | |||
| return $this->divQR($a, $b)[1]; | |||
| } | |||
| public function divQR(string $a, string $b) : array | |||
| { | |||
| if ($a === '0') { | |||
| return ['0', '0']; | |||
| } | |||
| if ($a === $b) { | |||
| return ['1', '0']; | |||
| } | |||
| if ($b === '1') { | |||
| return [$a, '0']; | |||
| } | |||
| if ($b === '-1') { | |||
| return [$this->neg($a), '0']; | |||
| } | |||
| /** @psalm-var numeric-string $a */ | |||
| $na = $a * 1; // cast to number | |||
| if (is_int($na)) { | |||
| /** @psalm-var numeric-string $b */ | |||
| $nb = $b * 1; | |||
| if (is_int($nb)) { | |||
| // the only division that may overflow is PHP_INT_MIN / -1, | |||
| // which cannot happen here as we've already handled a divisor of -1 above. | |||
| $q = intdiv($na, $nb); | |||
| $r = $na % $nb; | |||
| return [ | |||
| (string) $q, | |||
| (string) $r | |||
| ]; | |||
| } | |||
| } | |||
| [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); | |||
| [$q, $r] = $this->doDiv($aDig, $bDig); | |||
| if ($aNeg !== $bNeg) { | |||
| $q = $this->neg($q); | |||
| } | |||
| if ($aNeg) { | |||
| $r = $this->neg($r); | |||
| } | |||
| return [$q, $r]; | |||
| } | |||
| public function pow(string $a, int $e) : string | |||
| { | |||
| if ($e === 0) { | |||
| return '1'; | |||
| } | |||
| if ($e === 1) { | |||
| return $a; | |||
| } | |||
| $odd = $e % 2; | |||
| $e -= $odd; | |||
| $aa = $this->mul($a, $a); | |||
| /** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */ | |||
| $result = $this->pow($aa, $e / 2); | |||
| if ($odd === 1) { | |||
| $result = $this->mul($result, $a); | |||
| } | |||
| return $result; | |||
| } | |||
| /** | |||
| * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ | |||
| */ | |||
| public function modPow(string $base, string $exp, string $mod) : string | |||
| { | |||
| // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) | |||
| if ($base === '0' && $exp === '0' && $mod === '1') { | |||
| return '0'; | |||
| } | |||
| // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0) | |||
| if ($exp === '0' && $mod === '1') { | |||
| return '0'; | |||
| } | |||
| $x = $base; | |||
| $res = '1'; | |||
| // numbers are positive, so we can use remainder instead of modulo | |||
| $x = $this->divR($x, $mod); | |||
| while ($exp !== '0') { | |||
| if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd | |||
| $res = $this->divR($this->mul($res, $x), $mod); | |||
| } | |||
| $exp = $this->divQ($exp, '2'); | |||
| $x = $this->divR($this->mul($x, $x), $mod); | |||
| } | |||
| return $res; | |||
| } | |||
| /** | |||
| * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html | |||
| */ | |||
| public function sqrt(string $n) : string | |||
| { | |||
| if ($n === '0') { | |||
| return '0'; | |||
| } | |||
| // initial approximation | |||
| $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1); | |||
| $decreased = false; | |||
| for (;;) { | |||
| $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2'); | |||
| if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) { | |||
| break; | |||
| } | |||
| $decreased = $this->cmp($nx, $x) < 0; | |||
| $x = $nx; | |||
| } | |||
| return $x; | |||
| } | |||
| /** | |||
| * Performs the addition of two non-signed large integers. | |||
| */ | |||
| private function doAdd(string $a, string $b) : string | |||
| { | |||
| [$a, $b, $length] = $this->pad($a, $b); | |||
| $carry = 0; | |||
| $result = ''; | |||
| for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { | |||
| $blockLength = $this->maxDigits; | |||
| if ($i < 0) { | |||
| $blockLength += $i; | |||
| /** @psalm-suppress LoopInvalidation */ | |||
| $i = 0; | |||
| } | |||
| /** @psalm-var numeric-string $blockA */ | |||
| $blockA = \substr($a, $i, $blockLength); | |||
| /** @psalm-var numeric-string $blockB */ | |||
| $blockB = \substr($b, $i, $blockLength); | |||
| $sum = (string) ($blockA + $blockB + $carry); | |||
| $sumLength = \strlen($sum); | |||
| if ($sumLength > $blockLength) { | |||
| $sum = \substr($sum, 1); | |||
| $carry = 1; | |||
| } else { | |||
| if ($sumLength < $blockLength) { | |||
| $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; | |||
| } | |||
| $carry = 0; | |||
| } | |||
| $result = $sum . $result; | |||
| if ($i === 0) { | |||
| break; | |||
| } | |||
| } | |||
| if ($carry === 1) { | |||
| $result = '1' . $result; | |||
| } | |||
| return $result; | |||
| } | |||
| /** | |||
| * Performs the subtraction of two non-signed large integers. | |||
| */ | |||
| private function doSub(string $a, string $b) : string | |||
| { | |||
| if ($a === $b) { | |||
| return '0'; | |||
| } | |||
| // Ensure that we always subtract to a positive result: biggest minus smallest. | |||
| $cmp = $this->doCmp($a, $b); | |||
| $invert = ($cmp === -1); | |||
| if ($invert) { | |||
| $c = $a; | |||
| $a = $b; | |||
| $b = $c; | |||
| } | |||
| [$a, $b, $length] = $this->pad($a, $b); | |||
| $carry = 0; | |||
| $result = ''; | |||
| $complement = 10 ** $this->maxDigits; | |||
| for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { | |||
| $blockLength = $this->maxDigits; | |||
| if ($i < 0) { | |||
| $blockLength += $i; | |||
| /** @psalm-suppress LoopInvalidation */ | |||
| $i = 0; | |||
| } | |||
| /** @psalm-var numeric-string $blockA */ | |||
| $blockA = \substr($a, $i, $blockLength); | |||
| /** @psalm-var numeric-string $blockB */ | |||
| $blockB = \substr($b, $i, $blockLength); | |||
| $sum = $blockA - $blockB - $carry; | |||
| if ($sum < 0) { | |||
| $sum += $complement; | |||
| $carry = 1; | |||
| } else { | |||
| $carry = 0; | |||
| } | |||
| $sum = (string) $sum; | |||
| $sumLength = \strlen($sum); | |||
| if ($sumLength < $blockLength) { | |||
| $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; | |||
| } | |||
| $result = $sum . $result; | |||
| if ($i === 0) { | |||
| break; | |||
| } | |||
| } | |||
| // Carry cannot be 1 when the loop ends, as a > b | |||
| assert($carry === 0); | |||
| $result = \ltrim($result, '0'); | |||
| if ($invert) { | |||
| $result = $this->neg($result); | |||
| } | |||
| return $result; | |||
| } | |||
| /** | |||
| * Performs the multiplication of two non-signed large integers. | |||
| */ | |||
| private function doMul(string $a, string $b) : string | |||
| { | |||
| $x = \strlen($a); | |||
| $y = \strlen($b); | |||
| $maxDigits = \intdiv($this->maxDigits, 2); | |||
| $complement = 10 ** $maxDigits; | |||
| $result = '0'; | |||
| for ($i = $x - $maxDigits;; $i -= $maxDigits) { | |||
| $blockALength = $maxDigits; | |||
| if ($i < 0) { | |||
| $blockALength += $i; | |||
| /** @psalm-suppress LoopInvalidation */ | |||
| $i = 0; | |||
| } | |||
| $blockA = (int) \substr($a, $i, $blockALength); | |||
| $line = ''; | |||
| $carry = 0; | |||
| for ($j = $y - $maxDigits;; $j -= $maxDigits) { | |||
| $blockBLength = $maxDigits; | |||
| if ($j < 0) { | |||
| $blockBLength += $j; | |||
| /** @psalm-suppress LoopInvalidation */ | |||
| $j = 0; | |||
| } | |||
| $blockB = (int) \substr($b, $j, $blockBLength); | |||
| $mul = $blockA * $blockB + $carry; | |||
| $value = $mul % $complement; | |||
| $carry = ($mul - $value) / $complement; | |||
| $value = (string) $value; | |||
| $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT); | |||
| $line = $value . $line; | |||
| if ($j === 0) { | |||
| break; | |||
| } | |||
| } | |||
| if ($carry !== 0) { | |||
| $line = $carry . $line; | |||
| } | |||
| $line = \ltrim($line, '0'); | |||
| if ($line !== '') { | |||
| $line .= \str_repeat('0', $x - $blockALength - $i); | |||
| $result = $this->add($result, $line); | |||
| } | |||
| if ($i === 0) { | |||
| break; | |||
| } | |||
| } | |||
| return $result; | |||
| } | |||
| /** | |||
| * Performs the division of two non-signed large integers. | |||
| * | |||
| * @return string[] The quotient and remainder. | |||
| */ | |||
| private function doDiv(string $a, string $b) : array | |||
| { | |||
| $cmp = $this->doCmp($a, $b); | |||
| if ($cmp === -1) { | |||
| return ['0', $a]; | |||
| } | |||
| $x = \strlen($a); | |||
| $y = \strlen($b); | |||
| // we now know that a >= b && x >= y | |||
| $q = '0'; // quotient | |||
| $r = $a; // remainder | |||
| $z = $y; // focus length, always $y or $y+1 | |||
| for (;;) { | |||
| $focus = \substr($a, 0, $z); | |||
| $cmp = $this->doCmp($focus, $b); | |||
| if ($cmp === -1) { | |||
| if ($z === $x) { // remainder < dividend | |||
| break; | |||
| } | |||
| $z++; | |||
| } | |||
| $zeros = \str_repeat('0', $x - $z); | |||
| $q = $this->add($q, '1' . $zeros); | |||
| $a = $this->sub($a, $b . $zeros); | |||
| $r = $a; | |||
| if ($r === '0') { // remainder == 0 | |||
| break; | |||
| } | |||
| $x = \strlen($a); | |||
| if ($x < $y) { // remainder < dividend | |||
| break; | |||
| } | |||
| $z = $y; | |||
| } | |||
| return [$q, $r]; | |||
| } | |||
| /** | |||
| * Compares two non-signed large numbers. | |||
| * | |||
| * @psalm-return -1|0|1 | |||
| */ | |||
| private function doCmp(string $a, string $b) : int | |||
| { | |||
| $x = \strlen($a); | |||
| $y = \strlen($b); | |||
| $cmp = $x <=> $y; | |||
| if ($cmp !== 0) { | |||
| return $cmp; | |||
| } | |||
| return \strcmp($a, $b) <=> 0; // enforce -1|0|1 | |||
| } | |||
| /** | |||
| * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length. | |||
| * | |||
| * The numbers must only consist of digits, without leading minus sign. | |||
| * | |||
| * @return array{string, string, int} | |||
| */ | |||
| private function pad(string $a, string $b) : array | |||
| { | |||
| $x = \strlen($a); | |||
| $y = \strlen($b); | |||
| if ($x > $y) { | |||
| $b = \str_repeat('0', $x - $y) . $b; | |||
| return [$a, $b, $x]; | |||
| } | |||
| if ($x < $y) { | |||
| $a = \str_repeat('0', $y - $x) . $a; | |||
| return [$a, $b, $y]; | |||
| } | |||
| return [$a, $b, $x]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,98 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace Brick\Math; | |||
| /** | |||
| * Specifies a rounding behavior for numerical operations capable of discarding precision. | |||
| * | |||
| * Each rounding mode indicates how the least significant returned digit of a rounded result | |||
| * is to be calculated. If fewer digits are returned than the digits needed to represent the | |||
| * exact numerical result, the discarded digits will be referred to as the discarded fraction | |||
| * regardless the digits' contribution to the value of the number. In other words, considered | |||
| * as a numerical value, the discarded fraction could have an absolute value greater than one. | |||
| */ | |||
| enum RoundingMode | |||
| { | |||
| /** | |||
| * Asserts that the requested operation has an exact result, hence no rounding is necessary. | |||
| * | |||
| * If this rounding mode is specified on an operation that yields a result that | |||
| * cannot be represented at the requested scale, a RoundingNecessaryException is thrown. | |||
| */ | |||
| case UNNECESSARY; | |||
| /** | |||
| * Rounds away from zero. | |||
| * | |||
| * Always increments the digit prior to a nonzero discarded fraction. | |||
| * Note that this rounding mode never decreases the magnitude of the calculated value. | |||
| */ | |||
| case UP; | |||
| /** | |||
| * Rounds towards zero. | |||
| * | |||
| * Never increments the digit prior to a discarded fraction (i.e., truncates). | |||
| * Note that this rounding mode never increases the magnitude of the calculated value. | |||
| */ | |||
| case DOWN; | |||
| /** | |||
| * Rounds towards positive infinity. | |||
| * | |||
| * If the result is positive, behaves as for UP; if negative, behaves as for DOWN. | |||
| * Note that this rounding mode never decreases the calculated value. | |||
| */ | |||
| case CEILING; | |||
| /** | |||
| * Rounds towards negative infinity. | |||
| * | |||
| * If the result is positive, behave as for DOWN; if negative, behave as for UP. | |||
| * Note that this rounding mode never increases the calculated value. | |||
| */ | |||
| case FLOOR; | |||
| /** | |||
| * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. | |||
| * | |||
| * Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN. | |||
| * Note that this is the rounding mode commonly taught at school. | |||
| */ | |||
| case HALF_UP; | |||
| /** | |||
| * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. | |||
| * | |||
| * Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN. | |||
| */ | |||
| case HALF_DOWN; | |||
| /** | |||
| * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity. | |||
| * | |||
| * If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN. | |||
| */ | |||
| case HALF_CEILING; | |||
| /** | |||
| * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity. | |||
| * | |||
| * If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP. | |||
| */ | |||
| case HALF_FLOOR; | |||
| /** | |||
| * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor. | |||
| * | |||
| * Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd; | |||
| * behaves as for HALF_DOWN if it's even. | |||
| * | |||
| * Note that this is the rounding mode that statistically minimizes | |||
| * cumulative error when applied repeatedly over a sequence of calculations. | |||
| * It is sometimes known as "Banker's rounding", and is chiefly used in the USA. | |||
| */ | |||
| case HALF_EVEN; | |||
| } | |||
| @@ -0,0 +1,579 @@ | |||
| <?php | |||
| /* | |||
| * This file is part of Composer. | |||
| * | |||
| * (c) Nils Adermann <naderman@naderman.de> | |||
| * Jordi Boggiano <j.boggiano@seld.be> | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| */ | |||
| namespace Composer\Autoload; | |||
| /** | |||
| * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. | |||
| * | |||
| * $loader = new \Composer\Autoload\ClassLoader(); | |||
| * | |||
| * // register classes with namespaces | |||
| * $loader->add('Symfony\Component', __DIR__.'/component'); | |||
| * $loader->add('Symfony', __DIR__.'/framework'); | |||
| * | |||
| * // activate the autoloader | |||
| * $loader->register(); | |||
| * | |||
| * // to enable searching the include path (eg. for PEAR packages) | |||
| * $loader->setUseIncludePath(true); | |||
| * | |||
| * In this example, if you try to use a class in the Symfony\Component | |||
| * namespace or one of its children (Symfony\Component\Console for instance), | |||
| * the autoloader will first look for the class under the component/ | |||
| * directory, and it will then fallback to the framework/ directory if not | |||
| * found before giving up. | |||
| * | |||
| * This class is loosely based on the Symfony UniversalClassLoader. | |||
| * | |||
| * @author Fabien Potencier <fabien@symfony.com> | |||
| * @author Jordi Boggiano <j.boggiano@seld.be> | |||
| * @see https://www.php-fig.org/psr/psr-0/ | |||
| * @see https://www.php-fig.org/psr/psr-4/ | |||
| */ | |||
| class ClassLoader | |||
| { | |||
| /** @var \Closure(string):void */ | |||
| private static $includeFile; | |||
| /** @var string|null */ | |||
| private $vendorDir; | |||
| // PSR-4 | |||
| /** | |||
| * @var array<string, array<string, int>> | |||
| */ | |||
| private $prefixLengthsPsr4 = array(); | |||
| /** | |||
| * @var array<string, list<string>> | |||
| */ | |||
| private $prefixDirsPsr4 = array(); | |||
| /** | |||
| * @var list<string> | |||
| */ | |||
| private $fallbackDirsPsr4 = array(); | |||
| // PSR-0 | |||
| /** | |||
| * List of PSR-0 prefixes | |||
| * | |||
| * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) | |||
| * | |||
| * @var array<string, array<string, list<string>>> | |||
| */ | |||
| private $prefixesPsr0 = array(); | |||
| /** | |||
| * @var list<string> | |||
| */ | |||
| private $fallbackDirsPsr0 = array(); | |||
| /** @var bool */ | |||
| private $useIncludePath = false; | |||
| /** | |||
| * @var array<string, string> | |||
| */ | |||
| private $classMap = array(); | |||
| /** @var bool */ | |||
| private $classMapAuthoritative = false; | |||
| /** | |||
| * @var array<string, bool> | |||
| */ | |||
| private $missingClasses = array(); | |||
| /** @var string|null */ | |||
| private $apcuPrefix; | |||
| /** | |||
| * @var array<string, self> | |||
| */ | |||
| private static $registeredLoaders = array(); | |||
| /** | |||
| * @param string|null $vendorDir | |||
| */ | |||
| public function __construct($vendorDir = null) | |||
| { | |||
| $this->vendorDir = $vendorDir; | |||
| self::initializeIncludeClosure(); | |||
| } | |||
| /** | |||
| * @return array<string, list<string>> | |||
| */ | |||
| public function getPrefixes() | |||
| { | |||
| if (!empty($this->prefixesPsr0)) { | |||
| return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); | |||
| } | |||
| return array(); | |||
| } | |||
| /** | |||
| * @return array<string, list<string>> | |||
| */ | |||
| public function getPrefixesPsr4() | |||
| { | |||
| return $this->prefixDirsPsr4; | |||
| } | |||
| /** | |||
| * @return list<string> | |||
| */ | |||
| public function getFallbackDirs() | |||
| { | |||
| return $this->fallbackDirsPsr0; | |||
| } | |||
| /** | |||
| * @return list<string> | |||
| */ | |||
| public function getFallbackDirsPsr4() | |||
| { | |||
| return $this->fallbackDirsPsr4; | |||
| } | |||
| /** | |||
| * @return array<string, string> Array of classname => path | |||
| */ | |||
| public function getClassMap() | |||
| { | |||
| return $this->classMap; | |||
| } | |||
| /** | |||
| * @param array<string, string> $classMap Class to filename map | |||
| * | |||
| * @return void | |||
| */ | |||
| public function addClassMap(array $classMap) | |||
| { | |||
| if ($this->classMap) { | |||
| $this->classMap = array_merge($this->classMap, $classMap); | |||
| } else { | |||
| $this->classMap = $classMap; | |||
| } | |||
| } | |||
| /** | |||
| * Registers a set of PSR-0 directories for a given prefix, either | |||
| * appending or prepending to the ones previously set for this prefix. | |||
| * | |||
| * @param string $prefix The prefix | |||
| * @param list<string>|string $paths The PSR-0 root directories | |||
| * @param bool $prepend Whether to prepend the directories | |||
| * | |||
| * @return void | |||
| */ | |||
| public function add($prefix, $paths, $prepend = false) | |||
| { | |||
| $paths = (array) $paths; | |||
| if (!$prefix) { | |||
| if ($prepend) { | |||
| $this->fallbackDirsPsr0 = array_merge( | |||
| $paths, | |||
| $this->fallbackDirsPsr0 | |||
| ); | |||
| } else { | |||
| $this->fallbackDirsPsr0 = array_merge( | |||
| $this->fallbackDirsPsr0, | |||
| $paths | |||
| ); | |||
| } | |||
| return; | |||
| } | |||
| $first = $prefix[0]; | |||
| if (!isset($this->prefixesPsr0[$first][$prefix])) { | |||
| $this->prefixesPsr0[$first][$prefix] = $paths; | |||
| return; | |||
| } | |||
| if ($prepend) { | |||
| $this->prefixesPsr0[$first][$prefix] = array_merge( | |||
| $paths, | |||
| $this->prefixesPsr0[$first][$prefix] | |||
| ); | |||
| } else { | |||
| $this->prefixesPsr0[$first][$prefix] = array_merge( | |||
| $this->prefixesPsr0[$first][$prefix], | |||
| $paths | |||
| ); | |||
| } | |||
| } | |||
| /** | |||
| * Registers a set of PSR-4 directories for a given namespace, either | |||
| * appending or prepending to the ones previously set for this namespace. | |||
| * | |||
| * @param string $prefix The prefix/namespace, with trailing '\\' | |||
| * @param list<string>|string $paths The PSR-4 base directories | |||
| * @param bool $prepend Whether to prepend the directories | |||
| * | |||
| * @throws \InvalidArgumentException | |||
| * | |||
| * @return void | |||
| */ | |||
| public function addPsr4($prefix, $paths, $prepend = false) | |||
| { | |||
| $paths = (array) $paths; | |||
| if (!$prefix) { | |||
| // Register directories for the root namespace. | |||
| if ($prepend) { | |||
| $this->fallbackDirsPsr4 = array_merge( | |||
| $paths, | |||
| $this->fallbackDirsPsr4 | |||
| ); | |||
| } else { | |||
| $this->fallbackDirsPsr4 = array_merge( | |||
| $this->fallbackDirsPsr4, | |||
| $paths | |||
| ); | |||
| } | |||
| } elseif (!isset($this->prefixDirsPsr4[$prefix])) { | |||
| // Register directories for a new namespace. | |||
| $length = strlen($prefix); | |||
| if ('\\' !== $prefix[$length - 1]) { | |||
| throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); | |||
| } | |||
| $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; | |||
| $this->prefixDirsPsr4[$prefix] = $paths; | |||
| } elseif ($prepend) { | |||
| // Prepend directories for an already registered namespace. | |||
| $this->prefixDirsPsr4[$prefix] = array_merge( | |||
| $paths, | |||
| $this->prefixDirsPsr4[$prefix] | |||
| ); | |||
| } else { | |||
| // Append directories for an already registered namespace. | |||
| $this->prefixDirsPsr4[$prefix] = array_merge( | |||
| $this->prefixDirsPsr4[$prefix], | |||
| $paths | |||
| ); | |||
| } | |||
| } | |||
| /** | |||
| * Registers a set of PSR-0 directories for a given prefix, | |||
| * replacing any others previously set for this prefix. | |||
| * | |||
| * @param string $prefix The prefix | |||
| * @param list<string>|string $paths The PSR-0 base directories | |||
| * | |||
| * @return void | |||
| */ | |||
| public function set($prefix, $paths) | |||
| { | |||
| if (!$prefix) { | |||
| $this->fallbackDirsPsr0 = (array) $paths; | |||
| } else { | |||
| $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; | |||
| } | |||
| } | |||
| /** | |||
| * Registers a set of PSR-4 directories for a given namespace, | |||
| * replacing any others previously set for this namespace. | |||
| * | |||
| * @param string $prefix The prefix/namespace, with trailing '\\' | |||
| * @param list<string>|string $paths The PSR-4 base directories | |||
| * | |||
| * @throws \InvalidArgumentException | |||
| * | |||
| * @return void | |||
| */ | |||
| public function setPsr4($prefix, $paths) | |||
| { | |||
| if (!$prefix) { | |||
| $this->fallbackDirsPsr4 = (array) $paths; | |||
| } else { | |||
| $length = strlen($prefix); | |||
| if ('\\' !== $prefix[$length - 1]) { | |||
| throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); | |||
| } | |||
| $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; | |||
| $this->prefixDirsPsr4[$prefix] = (array) $paths; | |||
| } | |||
| } | |||
| /** | |||
| * Turns on searching the include path for class files. | |||
| * | |||
| * @param bool $useIncludePath | |||
| * | |||
| * @return void | |||
| */ | |||
| public function setUseIncludePath($useIncludePath) | |||
| { | |||
| $this->useIncludePath = $useIncludePath; | |||
| } | |||
| /** | |||
| * Can be used to check if the autoloader uses the include path to check | |||
| * for classes. | |||
| * | |||
| * @return bool | |||
| */ | |||
| public function getUseIncludePath() | |||
| { | |||
| return $this->useIncludePath; | |||
| } | |||
| /** | |||
| * Turns off searching the prefix and fallback directories for classes | |||
| * that have not been registered with the class map. | |||
| * | |||
| * @param bool $classMapAuthoritative | |||
| * | |||
| * @return void | |||
| */ | |||
| public function setClassMapAuthoritative($classMapAuthoritative) | |||
| { | |||
| $this->classMapAuthoritative = $classMapAuthoritative; | |||
| } | |||
| /** | |||
| * Should class lookup fail if not found in the current class map? | |||
| * | |||
| * @return bool | |||
| */ | |||
| public function isClassMapAuthoritative() | |||
| { | |||
| return $this->classMapAuthoritative; | |||
| } | |||
| /** | |||
| * APCu prefix to use to cache found/not-found classes, if the extension is enabled. | |||
| * | |||
| * @param string|null $apcuPrefix | |||
| * | |||
| * @return void | |||
| */ | |||
| public function setApcuPrefix($apcuPrefix) | |||
| { | |||
| $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; | |||
| } | |||
| /** | |||
| * The APCu prefix in use, or null if APCu caching is not enabled. | |||
| * | |||
| * @return string|null | |||
| */ | |||
| public function getApcuPrefix() | |||
| { | |||
| return $this->apcuPrefix; | |||
| } | |||
| /** | |||
| * Registers this instance as an autoloader. | |||
| * | |||
| * @param bool $prepend Whether to prepend the autoloader or not | |||
| * | |||
| * @return void | |||
| */ | |||
| public function register($prepend = false) | |||
| { | |||
| spl_autoload_register(array($this, 'loadClass'), true, $prepend); | |||
| if (null === $this->vendorDir) { | |||
| return; | |||
| } | |||
| if ($prepend) { | |||
| self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; | |||
| } else { | |||
| unset(self::$registeredLoaders[$this->vendorDir]); | |||
| self::$registeredLoaders[$this->vendorDir] = $this; | |||
| } | |||
| } | |||
| /** | |||
| * Unregisters this instance as an autoloader. | |||
| * | |||
| * @return void | |||
| */ | |||
| public function unregister() | |||
| { | |||
| spl_autoload_unregister(array($this, 'loadClass')); | |||
| if (null !== $this->vendorDir) { | |||
| unset(self::$registeredLoaders[$this->vendorDir]); | |||
| } | |||
| } | |||
| /** | |||
| * Loads the given class or interface. | |||
| * | |||
| * @param string $class The name of the class | |||
| * @return true|null True if loaded, null otherwise | |||
| */ | |||
| public function loadClass($class) | |||
| { | |||
| if ($file = $this->findFile($class)) { | |||
| $includeFile = self::$includeFile; | |||
| $includeFile($file); | |||
| return true; | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Finds the path to the file where the class is defined. | |||
| * | |||
| * @param string $class The name of the class | |||
| * | |||
| * @return string|false The path if found, false otherwise | |||
| */ | |||
| public function findFile($class) | |||
| { | |||
| // class map lookup | |||
| if (isset($this->classMap[$class])) { | |||
| return $this->classMap[$class]; | |||
| } | |||
| if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { | |||
| return false; | |||
| } | |||
| if (null !== $this->apcuPrefix) { | |||
| $file = apcu_fetch($this->apcuPrefix.$class, $hit); | |||
| if ($hit) { | |||
| return $file; | |||
| } | |||
| } | |||
| $file = $this->findFileWithExtension($class, '.php'); | |||
| // Search for Hack files if we are running on HHVM | |||
| if (false === $file && defined('HHVM_VERSION')) { | |||
| $file = $this->findFileWithExtension($class, '.hh'); | |||
| } | |||
| if (null !== $this->apcuPrefix) { | |||
| apcu_add($this->apcuPrefix.$class, $file); | |||
| } | |||
| if (false === $file) { | |||
| // Remember that this class does not exist. | |||
| $this->missingClasses[$class] = true; | |||
| } | |||
| return $file; | |||
| } | |||
| /** | |||
| * Returns the currently registered loaders keyed by their corresponding vendor directories. | |||
| * | |||
| * @return array<string, self> | |||
| */ | |||
| public static function getRegisteredLoaders() | |||
| { | |||
| return self::$registeredLoaders; | |||
| } | |||
| /** | |||
| * @param string $class | |||
| * @param string $ext | |||
| * @return string|false | |||
| */ | |||
| private function findFileWithExtension($class, $ext) | |||
| { | |||
| // PSR-4 lookup | |||
| $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; | |||
| $first = $class[0]; | |||
| if (isset($this->prefixLengthsPsr4[$first])) { | |||
| $subPath = $class; | |||
| while (false !== $lastPos = strrpos($subPath, '\\')) { | |||
| $subPath = substr($subPath, 0, $lastPos); | |||
| $search = $subPath . '\\'; | |||
| if (isset($this->prefixDirsPsr4[$search])) { | |||
| $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); | |||
| foreach ($this->prefixDirsPsr4[$search] as $dir) { | |||
| if (file_exists($file = $dir . $pathEnd)) { | |||
| return $file; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // PSR-4 fallback dirs | |||
| foreach ($this->fallbackDirsPsr4 as $dir) { | |||
| if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { | |||
| return $file; | |||
| } | |||
| } | |||
| // PSR-0 lookup | |||
| if (false !== $pos = strrpos($class, '\\')) { | |||
| // namespaced class name | |||
| $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) | |||
| . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); | |||
| } else { | |||
| // PEAR-like class name | |||
| $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; | |||
| } | |||
| if (isset($this->prefixesPsr0[$first])) { | |||
| foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { | |||
| if (0 === strpos($class, $prefix)) { | |||
| foreach ($dirs as $dir) { | |||
| if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { | |||
| return $file; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // PSR-0 fallback dirs | |||
| foreach ($this->fallbackDirsPsr0 as $dir) { | |||
| if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { | |||
| return $file; | |||
| } | |||
| } | |||
| // PSR-0 include paths. | |||
| if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { | |||
| return $file; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * @return void | |||
| */ | |||
| private static function initializeIncludeClosure() | |||
| { | |||
| if (self::$includeFile !== null) { | |||
| return; | |||
| } | |||
| /** | |||
| * Scope isolated include. | |||
| * | |||
| * Prevents access to $this/self from included files. | |||
| * | |||
| * @param string $file | |||
| * @return void | |||
| */ | |||
| self::$includeFile = \Closure::bind(static function($file) { | |||
| include $file; | |||
| }, null, null); | |||
| } | |||
| } | |||
| @@ -0,0 +1,359 @@ | |||
| <?php | |||
| /* | |||
| * This file is part of Composer. | |||
| * | |||
| * (c) Nils Adermann <naderman@naderman.de> | |||
| * Jordi Boggiano <j.boggiano@seld.be> | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| */ | |||
| namespace Composer; | |||
| use Composer\Autoload\ClassLoader; | |||
| use Composer\Semver\VersionParser; | |||
| /** | |||
| * This class is copied in every Composer installed project and available to all | |||
| * | |||
| * See also https://getcomposer.org/doc/07-runtime.md#installed-versions | |||
| * | |||
| * To require its presence, you can require `composer-runtime-api ^2.0` | |||
| * | |||
| * @final | |||
| */ | |||
| class InstalledVersions | |||
| { | |||
| /** | |||
| * @var mixed[]|null | |||
| * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null | |||
| */ | |||
| private static $installed; | |||
| /** | |||
| * @var bool|null | |||
| */ | |||
| private static $canGetVendors; | |||
| /** | |||
| * @var array[] | |||
| * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> | |||
| */ | |||
| private static $installedByVendor = array(); | |||
| /** | |||
| * Returns a list of all package names which are present, either by being installed, replaced or provided | |||
| * | |||
| * @return string[] | |||
| * @psalm-return list<string> | |||
| */ | |||
| public static function getInstalledPackages() | |||
| { | |||
| $packages = array(); | |||
| foreach (self::getInstalled() as $installed) { | |||
| $packages[] = array_keys($installed['versions']); | |||
| } | |||
| if (1 === \count($packages)) { | |||
| return $packages[0]; | |||
| } | |||
| return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); | |||
| } | |||
| /** | |||
| * Returns a list of all package names with a specific type e.g. 'library' | |||
| * | |||
| * @param string $type | |||
| * @return string[] | |||
| * @psalm-return list<string> | |||
| */ | |||
| public static function getInstalledPackagesByType($type) | |||
| { | |||
| $packagesByType = array(); | |||
| foreach (self::getInstalled() as $installed) { | |||
| foreach ($installed['versions'] as $name => $package) { | |||
| if (isset($package['type']) && $package['type'] === $type) { | |||
| $packagesByType[] = $name; | |||
| } | |||
| } | |||
| } | |||
| return $packagesByType; | |||
| } | |||
| /** | |||
| * Checks whether the given package is installed | |||
| * | |||
| * This also returns true if the package name is provided or replaced by another package | |||
| * | |||
| * @param string $packageName | |||
| * @param bool $includeDevRequirements | |||
| * @return bool | |||
| */ | |||
| public static function isInstalled($packageName, $includeDevRequirements = true) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (isset($installed['versions'][$packageName])) { | |||
| return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * Checks whether the given package satisfies a version constraint | |||
| * | |||
| * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: | |||
| * | |||
| * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') | |||
| * | |||
| * @param VersionParser $parser Install composer/semver to have access to this class and functionality | |||
| * @param string $packageName | |||
| * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package | |||
| * @return bool | |||
| */ | |||
| public static function satisfies(VersionParser $parser, $packageName, $constraint) | |||
| { | |||
| $constraint = $parser->parseConstraints((string) $constraint); | |||
| $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); | |||
| return $provided->matches($constraint); | |||
| } | |||
| /** | |||
| * Returns a version constraint representing all the range(s) which are installed for a given package | |||
| * | |||
| * It is easier to use this via isInstalled() with the $constraint argument if you need to check | |||
| * whether a given version of a package is installed, and not just whether it exists | |||
| * | |||
| * @param string $packageName | |||
| * @return string Version constraint usable with composer/semver | |||
| */ | |||
| public static function getVersionRanges($packageName) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (!isset($installed['versions'][$packageName])) { | |||
| continue; | |||
| } | |||
| $ranges = array(); | |||
| if (isset($installed['versions'][$packageName]['pretty_version'])) { | |||
| $ranges[] = $installed['versions'][$packageName]['pretty_version']; | |||
| } | |||
| if (array_key_exists('aliases', $installed['versions'][$packageName])) { | |||
| $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); | |||
| } | |||
| if (array_key_exists('replaced', $installed['versions'][$packageName])) { | |||
| $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); | |||
| } | |||
| if (array_key_exists('provided', $installed['versions'][$packageName])) { | |||
| $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); | |||
| } | |||
| return implode(' || ', $ranges); | |||
| } | |||
| throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
| } | |||
| /** | |||
| * @param string $packageName | |||
| * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present | |||
| */ | |||
| public static function getVersion($packageName) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (!isset($installed['versions'][$packageName])) { | |||
| continue; | |||
| } | |||
| if (!isset($installed['versions'][$packageName]['version'])) { | |||
| return null; | |||
| } | |||
| return $installed['versions'][$packageName]['version']; | |||
| } | |||
| throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
| } | |||
| /** | |||
| * @param string $packageName | |||
| * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present | |||
| */ | |||
| public static function getPrettyVersion($packageName) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (!isset($installed['versions'][$packageName])) { | |||
| continue; | |||
| } | |||
| if (!isset($installed['versions'][$packageName]['pretty_version'])) { | |||
| return null; | |||
| } | |||
| return $installed['versions'][$packageName]['pretty_version']; | |||
| } | |||
| throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
| } | |||
| /** | |||
| * @param string $packageName | |||
| * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference | |||
| */ | |||
| public static function getReference($packageName) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (!isset($installed['versions'][$packageName])) { | |||
| continue; | |||
| } | |||
| if (!isset($installed['versions'][$packageName]['reference'])) { | |||
| return null; | |||
| } | |||
| return $installed['versions'][$packageName]['reference']; | |||
| } | |||
| throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
| } | |||
| /** | |||
| * @param string $packageName | |||
| * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. | |||
| */ | |||
| public static function getInstallPath($packageName) | |||
| { | |||
| foreach (self::getInstalled() as $installed) { | |||
| if (!isset($installed['versions'][$packageName])) { | |||
| continue; | |||
| } | |||
| return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; | |||
| } | |||
| throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); | |||
| } | |||
| /** | |||
| * @return array | |||
| * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} | |||
| */ | |||
| public static function getRootPackage() | |||
| { | |||
| $installed = self::getInstalled(); | |||
| return $installed[0]['root']; | |||
| } | |||
| /** | |||
| * Returns the raw installed.php data for custom implementations | |||
| * | |||
| * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. | |||
| * @return array[] | |||
| * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} | |||
| */ | |||
| public static function getRawData() | |||
| { | |||
| @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); | |||
| if (null === self::$installed) { | |||
| // only require the installed.php file if this file is loaded from its dumped location, | |||
| // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 | |||
| if (substr(__DIR__, -8, 1) !== 'C') { | |||
| self::$installed = include __DIR__ . '/installed.php'; | |||
| } else { | |||
| self::$installed = array(); | |||
| } | |||
| } | |||
| return self::$installed; | |||
| } | |||
| /** | |||
| * Returns the raw data of all installed.php which are currently loaded for custom implementations | |||
| * | |||
| * @return array[] | |||
| * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> | |||
| */ | |||
| public static function getAllRawData() | |||
| { | |||
| return self::getInstalled(); | |||
| } | |||
| /** | |||
| * Lets you reload the static array from another file | |||
| * | |||
| * This is only useful for complex integrations in which a project needs to use | |||
| * this class but then also needs to execute another project's autoloader in process, | |||
| * and wants to ensure both projects have access to their version of installed.php. | |||
| * | |||
| * A typical case would be PHPUnit, where it would need to make sure it reads all | |||
| * the data it needs from this class, then call reload() with | |||
| * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure | |||
| * the project in which it runs can then also use this class safely, without | |||
| * interference between PHPUnit's dependencies and the project's dependencies. | |||
| * | |||
| * @param array[] $data A vendor/composer/installed.php data set | |||
| * @return void | |||
| * | |||
| * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data | |||
| */ | |||
| public static function reload($data) | |||
| { | |||
| self::$installed = $data; | |||
| self::$installedByVendor = array(); | |||
| } | |||
| /** | |||
| * @return array[] | |||
| * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> | |||
| */ | |||
| private static function getInstalled() | |||
| { | |||
| if (null === self::$canGetVendors) { | |||
| self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); | |||
| } | |||
| $installed = array(); | |||
| if (self::$canGetVendors) { | |||
| foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { | |||
| if (isset(self::$installedByVendor[$vendorDir])) { | |||
| $installed[] = self::$installedByVendor[$vendorDir]; | |||
| } elseif (is_file($vendorDir.'/composer/installed.php')) { | |||
| /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ | |||
| $required = require $vendorDir.'/composer/installed.php'; | |||
| $installed[] = self::$installedByVendor[$vendorDir] = $required; | |||
| if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { | |||
| self::$installed = $installed[count($installed) - 1]; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (null === self::$installed) { | |||
| // only require the installed.php file if this file is loaded from its dumped location, | |||
| // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 | |||
| if (substr(__DIR__, -8, 1) !== 'C') { | |||
| /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ | |||
| $required = require __DIR__ . '/installed.php'; | |||
| self::$installed = $required; | |||
| } else { | |||
| self::$installed = array(); | |||
| } | |||
| } | |||
| if (self::$installed !== array()) { | |||
| $installed[] = self::$installed; | |||
| } | |||
| return $installed; | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| Copyright (c) Nils Adermann, Jordi Boggiano | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is furnished | |||
| to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in all | |||
| copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
| THE SOFTWARE. | |||
| @@ -0,0 +1,10 @@ | |||
| <?php | |||
| // autoload_classmap.php @generated by Composer | |||
| $vendorDir = dirname(__DIR__); | |||
| $baseDir = dirname($vendorDir); | |||
| return array( | |||
| 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', | |||
| ); | |||
| @@ -0,0 +1,10 @@ | |||
| <?php | |||
| // autoload_files.php @generated by Composer | |||
| $vendorDir = dirname(__DIR__); | |||
| $baseDir = dirname($vendorDir); | |||
| return array( | |||
| 'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php', | |||
| ); | |||
| @@ -0,0 +1,9 @@ | |||
| <?php | |||
| // autoload_namespaces.php @generated by Composer | |||
| $vendorDir = dirname(__DIR__); | |||
| $baseDir = dirname($vendorDir); | |||
| return array( | |||
| ); | |||
| @@ -0,0 +1,12 @@ | |||
| <?php | |||
| // autoload_psr4.php @generated by Composer | |||
| $vendorDir = dirname(__DIR__); | |||
| $baseDir = dirname($vendorDir); | |||
| return array( | |||
| 'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'), | |||
| 'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'), | |||
| 'Brick\\Math\\' => array($vendorDir . '/brick/math/src'), | |||
| ); | |||
| @@ -0,0 +1,50 @@ | |||
| <?php | |||
| // autoload_real.php @generated by Composer | |||
| class ComposerAutoloaderInit7422faf8f33d1f1cdfca8767e0ffc0f4 | |||
| { | |||
| private static $loader; | |||
| public static function loadClassLoader($class) | |||
| { | |||
| if ('Composer\Autoload\ClassLoader' === $class) { | |||
| require __DIR__ . '/ClassLoader.php'; | |||
| } | |||
| } | |||
| /** | |||
| * @return \Composer\Autoload\ClassLoader | |||
| */ | |||
| public static function getLoader() | |||
| { | |||
| if (null !== self::$loader) { | |||
| return self::$loader; | |||
| } | |||
| require __DIR__ . '/platform_check.php'; | |||
| spl_autoload_register(array('ComposerAutoloaderInit7422faf8f33d1f1cdfca8767e0ffc0f4', 'loadClassLoader'), true, true); | |||
| self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); | |||
| spl_autoload_unregister(array('ComposerAutoloaderInit7422faf8f33d1f1cdfca8767e0ffc0f4', 'loadClassLoader')); | |||
| require __DIR__ . '/autoload_static.php'; | |||
| call_user_func(\Composer\Autoload\ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4::getInitializer($loader)); | |||
| $loader->register(true); | |||
| $filesToLoad = \Composer\Autoload\ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4::$files; | |||
| $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { | |||
| if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { | |||
| $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; | |||
| require $file; | |||
| } | |||
| }, null, null); | |||
| foreach ($filesToLoad as $fileIdentifier => $file) { | |||
| $requireFile($fileIdentifier, $file); | |||
| } | |||
| return $loader; | |||
| } | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| <?php | |||
| // autoload_static.php @generated by Composer | |||
| namespace Composer\Autoload; | |||
| class ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4 | |||
| { | |||
| public static $files = array ( | |||
| 'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php', | |||
| ); | |||
| public static $prefixLengthsPsr4 = array ( | |||
| 'R' => | |||
| array ( | |||
| 'Ramsey\\Uuid\\' => 12, | |||
| 'Ramsey\\Collection\\' => 18, | |||
| ), | |||
| 'B' => | |||
| array ( | |||
| 'Brick\\Math\\' => 11, | |||
| ), | |||
| ); | |||
| public static $prefixDirsPsr4 = array ( | |||
| 'Ramsey\\Uuid\\' => | |||
| array ( | |||
| 0 => __DIR__ . '/..' . '/ramsey/uuid/src', | |||
| ), | |||
| 'Ramsey\\Collection\\' => | |||
| array ( | |||
| 0 => __DIR__ . '/..' . '/ramsey/collection/src', | |||
| ), | |||
| 'Brick\\Math\\' => | |||
| array ( | |||
| 0 => __DIR__ . '/..' . '/brick/math/src', | |||
| ), | |||
| ); | |||
| public static $classMap = array ( | |||
| 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', | |||
| ); | |||
| public static function getInitializer(ClassLoader $loader) | |||
| { | |||
| return \Closure::bind(function () use ($loader) { | |||
| $loader->prefixLengthsPsr4 = ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4::$prefixLengthsPsr4; | |||
| $loader->prefixDirsPsr4 = ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4::$prefixDirsPsr4; | |||
| $loader->classMap = ComposerStaticInit7422faf8f33d1f1cdfca8767e0ffc0f4::$classMap; | |||
| }, null, ClassLoader::class); | |||
| } | |||
| } | |||
| @@ -0,0 +1,256 @@ | |||
| { | |||
| "packages": [ | |||
| { | |||
| "name": "brick/math", | |||
| "version": "0.12.1", | |||
| "version_normalized": "0.12.1.0", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/brick/math.git", | |||
| "reference": "f510c0a40911935b77b86859eb5223d58d660df1" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", | |||
| "reference": "f510c0a40911935b77b86859eb5223d58d660df1", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "php-coveralls/php-coveralls": "^2.2", | |||
| "phpunit/phpunit": "^10.1", | |||
| "vimeo/psalm": "5.16.0" | |||
| }, | |||
| "time": "2023-11-29T23:19:16+00:00", | |||
| "type": "library", | |||
| "installation-source": "dist", | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Brick\\Math\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "description": "Arbitrary-precision arithmetic library", | |||
| "keywords": [ | |||
| "Arbitrary-precision", | |||
| "BigInteger", | |||
| "BigRational", | |||
| "arithmetic", | |||
| "bigdecimal", | |||
| "bignum", | |||
| "bignumber", | |||
| "brick", | |||
| "decimal", | |||
| "integer", | |||
| "math", | |||
| "mathematics", | |||
| "rational" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/brick/math/issues", | |||
| "source": "https://github.com/brick/math/tree/0.12.1" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/BenMorel", | |||
| "type": "github" | |||
| } | |||
| ], | |||
| "install-path": "../brick/math" | |||
| }, | |||
| { | |||
| "name": "ramsey/collection", | |||
| "version": "2.0.0", | |||
| "version_normalized": "2.0.0.0", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/ramsey/collection.git", | |||
| "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", | |||
| "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "ergebnis/composer-normalize": "^2.28.3", | |||
| "fakerphp/faker": "^1.21", | |||
| "hamcrest/hamcrest-php": "^2.0", | |||
| "jangregor/phpstan-prophecy": "^1.0", | |||
| "mockery/mockery": "^1.5", | |||
| "php-parallel-lint/php-console-highlighter": "^1.0", | |||
| "php-parallel-lint/php-parallel-lint": "^1.3", | |||
| "phpcsstandards/phpcsutils": "^1.0.0-rc1", | |||
| "phpspec/prophecy-phpunit": "^2.0", | |||
| "phpstan/extension-installer": "^1.2", | |||
| "phpstan/phpstan": "^1.9", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.3", | |||
| "phpunit/phpunit": "^9.5", | |||
| "psalm/plugin-mockery": "^1.1", | |||
| "psalm/plugin-phpunit": "^0.18.4", | |||
| "ramsey/coding-standard": "^2.0.3", | |||
| "ramsey/conventional-commits": "^1.3", | |||
| "vimeo/psalm": "^5.4" | |||
| }, | |||
| "time": "2022-12-31T21:50:55+00:00", | |||
| "type": "library", | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| }, | |||
| "ramsey/conventional-commits": { | |||
| "configFile": "conventional-commits.json" | |||
| } | |||
| }, | |||
| "installation-source": "dist", | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Ramsey\\Collection\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "authors": [ | |||
| { | |||
| "name": "Ben Ramsey", | |||
| "email": "ben@benramsey.com", | |||
| "homepage": "https://benramsey.com" | |||
| } | |||
| ], | |||
| "description": "A PHP library for representing and manipulating collections.", | |||
| "keywords": [ | |||
| "array", | |||
| "collection", | |||
| "hash", | |||
| "map", | |||
| "queue", | |||
| "set" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/ramsey/collection/issues", | |||
| "source": "https://github.com/ramsey/collection/tree/2.0.0" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/ramsey", | |||
| "type": "github" | |||
| }, | |||
| { | |||
| "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", | |||
| "type": "tidelift" | |||
| } | |||
| ], | |||
| "install-path": "../ramsey/collection" | |||
| }, | |||
| { | |||
| "name": "ramsey/uuid", | |||
| "version": "4.7.6", | |||
| "version_normalized": "4.7.6.0", | |||
| "source": { | |||
| "type": "git", | |||
| "url": "https://github.com/ramsey/uuid.git", | |||
| "reference": "91039bc1faa45ba123c4328958e620d382ec7088" | |||
| }, | |||
| "dist": { | |||
| "type": "zip", | |||
| "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", | |||
| "reference": "91039bc1faa45ba123c4328958e620d382ec7088", | |||
| "shasum": "" | |||
| }, | |||
| "require": { | |||
| "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", | |||
| "ext-json": "*", | |||
| "php": "^8.0", | |||
| "ramsey/collection": "^1.2 || ^2.0" | |||
| }, | |||
| "replace": { | |||
| "rhumsaa/uuid": "self.version" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/captainhook": "^5.10", | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", | |||
| "doctrine/annotations": "^1.8", | |||
| "ergebnis/composer-normalize": "^2.15", | |||
| "mockery/mockery": "^1.3", | |||
| "paragonie/random-lib": "^2", | |||
| "php-mock/php-mock": "^2.2", | |||
| "php-mock/php-mock-mockery": "^1.3", | |||
| "php-parallel-lint/php-parallel-lint": "^1.1", | |||
| "phpbench/phpbench": "^1.0", | |||
| "phpstan/extension-installer": "^1.1", | |||
| "phpstan/phpstan": "^1.8", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.1", | |||
| "phpunit/phpunit": "^8.5 || ^9", | |||
| "ramsey/composer-repl": "^1.4", | |||
| "slevomat/coding-standard": "^8.4", | |||
| "squizlabs/php_codesniffer": "^3.5", | |||
| "vimeo/psalm": "^4.9" | |||
| }, | |||
| "suggest": { | |||
| "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", | |||
| "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", | |||
| "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", | |||
| "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", | |||
| "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." | |||
| }, | |||
| "time": "2024-04-27T21:32:50+00:00", | |||
| "type": "library", | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| } | |||
| }, | |||
| "installation-source": "dist", | |||
| "autoload": { | |||
| "files": [ | |||
| "src/functions.php" | |||
| ], | |||
| "psr-4": { | |||
| "Ramsey\\Uuid\\": "src/" | |||
| } | |||
| }, | |||
| "notification-url": "https://packagist.org/downloads/", | |||
| "license": [ | |||
| "MIT" | |||
| ], | |||
| "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", | |||
| "keywords": [ | |||
| "guid", | |||
| "identifier", | |||
| "uuid" | |||
| ], | |||
| "support": { | |||
| "issues": "https://github.com/ramsey/uuid/issues", | |||
| "source": "https://github.com/ramsey/uuid/tree/4.7.6" | |||
| }, | |||
| "funding": [ | |||
| { | |||
| "url": "https://github.com/ramsey", | |||
| "type": "github" | |||
| }, | |||
| { | |||
| "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", | |||
| "type": "tidelift" | |||
| } | |||
| ], | |||
| "install-path": "../ramsey/uuid" | |||
| } | |||
| ], | |||
| "dev": true, | |||
| "dev-package-names": [] | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| <?php return array( | |||
| 'root' => array( | |||
| 'name' => '__root__', | |||
| 'pretty_version' => 'dev-master', | |||
| 'version' => 'dev-master', | |||
| 'reference' => '58250a9b460c2aa8b4e02c2feff7f72cb980a184', | |||
| 'type' => 'library', | |||
| 'install_path' => __DIR__ . '/../../', | |||
| 'aliases' => array(), | |||
| 'dev' => true, | |||
| ), | |||
| 'versions' => array( | |||
| '__root__' => array( | |||
| 'pretty_version' => 'dev-master', | |||
| 'version' => 'dev-master', | |||
| 'reference' => '58250a9b460c2aa8b4e02c2feff7f72cb980a184', | |||
| 'type' => 'library', | |||
| 'install_path' => __DIR__ . '/../../', | |||
| 'aliases' => array(), | |||
| 'dev_requirement' => false, | |||
| ), | |||
| 'brick/math' => array( | |||
| 'pretty_version' => '0.12.1', | |||
| 'version' => '0.12.1.0', | |||
| 'reference' => 'f510c0a40911935b77b86859eb5223d58d660df1', | |||
| 'type' => 'library', | |||
| 'install_path' => __DIR__ . '/../brick/math', | |||
| 'aliases' => array(), | |||
| 'dev_requirement' => false, | |||
| ), | |||
| 'ramsey/collection' => array( | |||
| 'pretty_version' => '2.0.0', | |||
| 'version' => '2.0.0.0', | |||
| 'reference' => 'a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5', | |||
| 'type' => 'library', | |||
| 'install_path' => __DIR__ . '/../ramsey/collection', | |||
| 'aliases' => array(), | |||
| 'dev_requirement' => false, | |||
| ), | |||
| 'ramsey/uuid' => array( | |||
| 'pretty_version' => '4.7.6', | |||
| 'version' => '4.7.6.0', | |||
| 'reference' => '91039bc1faa45ba123c4328958e620d382ec7088', | |||
| 'type' => 'library', | |||
| 'install_path' => __DIR__ . '/../ramsey/uuid', | |||
| 'aliases' => array(), | |||
| 'dev_requirement' => false, | |||
| ), | |||
| 'rhumsaa/uuid' => array( | |||
| 'dev_requirement' => false, | |||
| 'replaced' => array( | |||
| 0 => '4.7.6', | |||
| ), | |||
| ), | |||
| ), | |||
| ); | |||
| @@ -0,0 +1,26 @@ | |||
| <?php | |||
| // platform_check.php @generated by Composer | |||
| $issues = array(); | |||
| if (!(PHP_VERSION_ID >= 80100)) { | |||
| $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; | |||
| } | |||
| if ($issues) { | |||
| if (!headers_sent()) { | |||
| header('HTTP/1.1 500 Internal Server Error'); | |||
| } | |||
| if (!ini_get('display_errors')) { | |||
| if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { | |||
| fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); | |||
| } elseif (!headers_sent()) { | |||
| echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; | |||
| } | |||
| } | |||
| trigger_error( | |||
| 'Composer detected issues in your platform: ' . implode(' ', $issues), | |||
| E_USER_ERROR | |||
| ); | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| Copyright (c) 2015-2022 Ben Ramsey <ben@benramsey.com> | |||
| 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. | |||
| @@ -0,0 +1,70 @@ | |||
| <h1 align="center">ramsey/collection</h1> | |||
| <p align="center"> | |||
| <strong>A PHP library for representing and manipulating collections.</strong> | |||
| </p> | |||
| <p align="center"> | |||
| <a href="https://github.com/ramsey/collection"><img src="http://img.shields.io/badge/source-ramsey/collection-blue.svg?style=flat-square" alt="Source Code"></a> | |||
| <a href="https://packagist.org/packages/ramsey/collection"><img src="https://img.shields.io/packagist/v/ramsey/collection.svg?style=flat-square&label=release" alt="Download Package"></a> | |||
| <a href="https://php.net"><img src="https://img.shields.io/packagist/php-v/ramsey/collection.svg?style=flat-square&colorB=%238892BF" alt="PHP Programming Language"></a> | |||
| <a href="https://github.com/ramsey/collection/blob/master/LICENSE"><img src="https://img.shields.io/packagist/l/ramsey/collection.svg?style=flat-square&colorB=darkcyan" alt="Read License"></a> | |||
| <a href="https://github.com/ramsey/collection/actions/workflows/continuous-integration.yml"><img src="https://img.shields.io/github/actions/workflow/status/ramsey/collection/continuous-integration.yml?branch=main&logo=github&style=flat-square" alt="Build Status"></a> | |||
| <a href="https://codecov.io/gh/ramsey/collection"><img src="https://img.shields.io/codecov/c/gh/ramsey/collection?label=codecov&logo=codecov&style=flat-square" alt="Codecov Code Coverage"></a> | |||
| <a href="https://shepherd.dev/github/ramsey/collection"><img src="https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fshepherd.dev%2Fgithub%2Framsey%2Fcollection%2Fcoverage" alt="Psalm Type Coverage"></a> | |||
| </p> | |||
| ## About | |||
| ramsey/collection is a PHP library for representing and manipulating collections. | |||
| Much inspiration for this library came from the [Java Collections Framework][java]. | |||
| This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). | |||
| By participating in this project and its community, you are expected to | |||
| uphold this code. | |||
| ## Installation | |||
| Install this package as a dependency using [Composer](https://getcomposer.org). | |||
| ``` bash | |||
| composer require ramsey/collection | |||
| ``` | |||
| ## Usage | |||
| Examples of how to use this library may be found in the | |||
| [Wiki pages](https://github.com/ramsey/collection/wiki/Examples). | |||
| ## Contributing | |||
| Contributions are welcome! To contribute, please familiarize yourself with | |||
| [CONTRIBUTING.md](CONTRIBUTING.md). | |||
| ## Coordinated Disclosure | |||
| Keeping user information safe and secure is a top priority, and we welcome the | |||
| contribution of external security researchers. If you believe you've found a | |||
| security issue in software that is maintained in this repository, please read | |||
| [SECURITY.md][] for instructions on submitting a vulnerability report. | |||
| ## ramsey/collection for Enterprise | |||
| Available as part of the Tidelift Subscription. | |||
| The maintainers of ramsey/collection and thousands of other packages are working | |||
| with Tidelift to deliver commercial support and maintenance for the open source | |||
| packages you use to build your applications. Save time, reduce risk, and improve | |||
| code health, while paying the maintainers of the exact packages you use. | |||
| [Learn more.](https://tidelift.com/subscription/pkg/packagist-ramsey-collection?utm_source=undefined&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) | |||
| ## Copyright and License | |||
| The ramsey/collection library is copyright © [Ben Ramsey](https://benramsey.com) | |||
| and licensed for use under the terms of the | |||
| MIT License (MIT). Please see [LICENSE](LICENSE) for more information. | |||
| [java]: http://docs.oracle.com/javase/8/docs/technotes/guides/collections/index.html | |||
| [security.md]: https://github.com/ramsey/collection/blob/main/SECURITY.md | |||
| @@ -0,0 +1,169 @@ | |||
| <!-- | |||
| This policy template was created using the HackerOne Policy Builder [1], | |||
| with guidance from the National Telecommunications and Information | |||
| Administration Coordinated Vulnerability Disclosure Template [2]. | |||
| --> | |||
| # Vulnerability Disclosure Policy (VDP) | |||
| ## Brand Promise | |||
| <!-- | |||
| This is your brand promise. Its objective is to "demonstrate a clear, good | |||
| faith commitment to customers and other stakeholders potentially impacted by | |||
| security vulnerabilities" [2]. | |||
| --> | |||
| Keeping user information safe and secure is a top priority, and we welcome the | |||
| contribution of external security researchers. | |||
| ## Scope | |||
| <!-- | |||
| This is your initial scope. It tells vulnerability finders and reporters | |||
| "which systems and capabilities are 'fair game' versus 'off limits'" [2]. | |||
| For software packages, this is often a list of currently maintained versions | |||
| of the package. | |||
| --> | |||
| If you believe you've found a security issue in software that is maintained in | |||
| this repository, we encourage you to notify us. | |||
| | Version | In scope | Source code | | |||
| | ------- | :------: | ----------- | | |||
| | latest | ✅ | https://github.com/ramsey/collection | | |||
| ## How to Submit a Report | |||
| <!-- | |||
| This is your communication process. It tells security researchers how to | |||
| contact you to report a vulnerability. It may be a link to a web form that | |||
| uses HTTPS for secure communication, or it may be an email address. | |||
| Optionally, you may choose to include a PGP public key, so that researchers | |||
| may send you encrypted messages. | |||
| --> | |||
| To submit a vulnerability report, please contact us at security@ramsey.dev. | |||
| Your submission will be reviewed and validated by a member of our team. | |||
| ## Safe Harbor | |||
| <!-- | |||
| This section assures vulnerability finders and reporters that they will | |||
| receive good faith responses to their good faith acts. In other words, | |||
| "we will not take legal action if..." [2]. | |||
| --> | |||
| We support safe harbor for security researchers who: | |||
| * Make a good faith effort to avoid privacy violations, destruction of data, and | |||
| interruption or degradation of our services. | |||
| * Only interact with accounts you own or with explicit permission of the account | |||
| holder. If you do encounter Personally Identifiable Information (PII) contact | |||
| us immediately, do not proceed with access, and immediately purge any local | |||
| information. | |||
| * Provide us with a reasonable amount of time to resolve vulnerabilities prior | |||
| to any disclosure to the public or a third party. | |||
| We will consider activities conducted consistent with this policy to constitute | |||
| "authorized" conduct and will not pursue civil action or initiate a complaint to | |||
| law enforcement. We will help to the extent we can if legal action is initiated | |||
| by a third party against you. | |||
| Please submit a report to us before engaging in conduct that may be inconsistent | |||
| with or unaddressed by this policy. | |||
| ## Preferences | |||
| <!-- | |||
| The preferences section sets expectations based on priority and submission | |||
| volume, rather than legal objection or restriction [2]. | |||
| According to the NTIA [2]: | |||
| This section is a living document that sets expectations for preferences | |||
| and priorities, typically maintained by the support and engineering | |||
| team. This can outline classes of vulnerabilities, reporting style | |||
| (crash dumps, CVSS scoring, proof-of-concept, etc.), tools, etc. Too | |||
| many preferences can set the wrong tone or make reporting findings | |||
| difficult to navigate. This section also sets expectations to the | |||
| researcher community for what types of issues are considered important | |||
| or not. | |||
| --> | |||
| * Please provide detailed reports with reproducible steps and a clearly defined | |||
| impact. | |||
| * Include the version number of the vulnerable package in your report | |||
| * Social engineering (e.g. phishing, vishing, smishing) is prohibited. | |||
| <!-- | |||
| References | |||
| [1] HackerOne. Policy builder. Retrieved from https://hackerone.com/policy-builder/ | |||
| [2] NTIA Safety Working Group. 2016. "Early stage" coordinated vulnerability | |||
| disclosure template: Version 1.1. (15 December 2016). Retrieved from | |||
| https://www.ntia.doc.gov/files/ntia/publications/ntia_vuln_disclosure_early_stage_template.pdf | |||
| --> | |||
| ## Encryption Key for security@ramsey.dev | |||
| For increased privacy when reporting sensitive issues, you may encrypt your | |||
| message using the following public key: | |||
| ``` | |||
| -----BEGIN PGP PUBLIC KEY BLOCK----- | |||
| mQINBF+Z9gEBEACbT/pIx8RR0K18t8Z2rDnmEV44YdT7HNsMdq+D6SAlx8UUb6AU | |||
| jGIbV9dgBgGNtOLU1pxloaJwL9bWIRbj+X/Qb2WNIP//Vz1Y40ox1dSpfCUrizXx | |||
| kb4p58Xml0PsB8dg3b4RDUgKwGC37ne5xmDnigyJPbiB2XJ6Xc46oPCjh86XROTK | |||
| wEBB2lY67ClBlSlvC2V9KmbTboRQkLdQDhOaUosMb99zRb0EWqDLaFkZVjY5HI7i | |||
| 0pTveE6dI12NfHhTwKjZ5pUiAZQGlKA6J1dMjY2unxHZkQj5MlMfrLSyJHZxccdJ | |||
| xD94T6OTcTHt/XmMpI2AObpewZDdChDQmcYDZXGfAhFoJmbvXsmLMGXKgzKoZ/ls | |||
| RmLsQhh7+/r8E+Pn5r+A6Hh4uAc14ApyEP0ckKeIXw1C6pepHM4E8TEXVr/IA6K/ | |||
| z6jlHORixIFX7iNOnfHh+qwOgZw40D6JnBfEzjFi+T2Cy+JzN2uy7I8UnecTMGo3 | |||
| 5t6astPy6xcH6kZYzFTV7XERR6LIIVyLAiMFd8kF5MbJ8N5ElRFsFHPW+82N2HDX | |||
| c60iSaTB85k6R6xd8JIKDiaKE4sSuw2wHFCKq33d/GamYezp1wO+bVUQg88efljC | |||
| 2JNFyD+vl30josqhw1HcmbE1TP3DlYeIL5jQOlxCMsgai6JtTfHFM/5MYwARAQAB | |||
| tBNzZWN1cml0eUByYW1zZXkuZGV2iQJUBBMBCAA+FiEE4drPD+/ofZ570fAYq0bv | |||
| vXQCywIFAl+Z9gECGwMFCQeGH4AFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ | |||
| q0bvvXQCywIkEA//Qcwv8MtTCy01LHZd9c7VslwhNdXQDYymcTyjcYw8x7O22m4B | |||
| 3hXE6vqAplFhVxxkqXB2ef0tQuzxhPHNJgkCE4Wq4i+V6qGpaSVHQT2W6DN/NIhL | |||
| vS8OdScc6zddmIbIkSrzVVAtjwehFNEIrX3DnbbbK+Iku7vsKT5EclOluIsjlYoX | |||
| goW8IeReyDBqOe2H3hoCGw6EA0D/NYV2bJnfy53rXVIyarsXXeOLp7eNEH6Td7aW | |||
| PVSrMZJe1t+knrEGnEdrXWzlg4lCJJCtemGv+pKBUomnyISXSdqyoRCCzvQjqyig | |||
| 2kRebUX8BXPW33p4OXPj9sIboUOjZwormWwqqbFMO+J4TiVCUoEoheI7emPFRcNN | |||
| QtPJrjbY1++OznBc0GRpfeUkGoU1cbRl1bnepnFIZMTDLkrVW6I1Y4q8ZVwX3BkE | |||
| N81ctFrRpHBlU36EdHvjPQmGtuiL77Qq3fWmMv7yTvK1wHJAXfEb0ZJWHZCbck3w | |||
| l0CVq0Z+UUAOM8Rp1N0N8m92xtapav0qCFU9qzf2J5qX6GRmWv+d29wPgFHzDWBm | |||
| nnrYYIA4wJLx00U6SMcVBSnNe91B+RfGY5XQhbWPjQQecOGCSDsxaFAq2MeOVJyZ | |||
| bIjLYfG9GxoLKr5R7oLRJvZI4nKKBc1Kci/crZbdiSdQhSQGlDz88F1OHeCIdQQQ | |||
| EQgAHRYhBOhdAxHd+lus86YQ57Atl5icjAcbBQJfmfdIAAoJELAtl5icjAcbFVcA | |||
| /1LqB3ZjsnXDAvvAXZVjSPqofSlpMLeRQP6IM/A9Odq0AQCZrtZc1knOMGEcjppK | |||
| Rk+sy/R0Mshy8TDuaZIRgh2Ux7kCDQRfmfYBARAAmchKzzVz7IaEq7PnZDb3szQs | |||
| T/+E9F3m39yOpV4fEB1YzObonFakXNT7Gw2tZEx0eitUMqQ/13jjfu3UdzlKl2bR | |||
| qA8LrSQRhB+PTC9A1XvwxCUYhhjGiLzJ9CZL6hBQB43qHOmE9XJPme90geLsF+gK | |||
| u39Waj1SNWzwGg+Gy1Gl5f2AJoDTxznreCuFGj+Vfaczt/hlfgqpOdb9jsmdoE7t | |||
| 3DSWppA9dRHWwQSgE6J28rR4QySBcqyXS6IMykqaJn7Z26yNIaITLnHCZOSY8zhP | |||
| ha7GFsN549EOCgECbrnPt9dmI2+hQE0RO0e7SOBNsIf5sz/i7urhwuj0CbOqhjc2 | |||
| X1AEVNFCVcb6HPi/AWefdFCRu0gaWQxn5g+9nkq5slEgvzCCiKYzaBIcr8qR6Hb4 | |||
| FaOPVPxO8vndRouq57Ws8XpAwbPttioFuCqF4u9K+tK/8e2/R8QgRYJsE3Cz/Fu8 | |||
| +pZFpMnqbDEbK3DL3ss+1ed1sky+mDV8qXXeI33XW5hMFnk1JWshUjHNlQmE6ftC | |||
| U0xSTMVUtwJhzH2zDp8lEdu7qi3EsNULOl68ozDr6soWAvCbHPeTdTOnFySGCleG | |||
| /3TonsoZJs/sSPPJnxFQ1DtgQL6EbhIwa0ZwU4eKYVHZ9tjxuMX3teFzRvOrJjgs | |||
| +ywGlsIURtEckT5Y6nMAEQEAAYkCPAQYAQgAJhYhBOHazw/v6H2ee9HwGKtG7710 | |||
| AssCBQJfmfYBAhsMBQkHhh+AAAoJEKtG7710AssC8NcP/iDAcy1aZFvkA0EbZ85p | |||
| i7/+ywtE/1wF4U4/9OuLcoskqGGnl1pJNPooMOSBCfreoTB8HimT0Fln0CoaOm4Q | |||
| pScNq39JXmf4VxauqUJVARByP6zUfgYarqoaZNeuFF0S4AZJ2HhGzaQPjDz1uKVM | |||
| PE6tQSgQkFzdZ9AtRA4vElTH6yRAgmepUsOihk0b0gUtVnwtRYZ8e0Qt3ie97a73 | |||
| DxLgAgedFRUbLRYiT0vNaYbainBsLWKpN/T8odwIg/smP0Khjp/ckV60cZTdBiPR | |||
| szBTPJESMUTu0VPntc4gWwGsmhZJg/Tt/qP08XYo3VxNYBegyuWwNR66zDWvwvGH | |||
| muMv5UchuDxp6Rt3JkIO4voMT1JSjWy9p8krkPEE4V6PxAagLjdZSkt92wVLiK5x | |||
| y5gNrtPhU45YdRAKHr36OvJBJQ42CDaZ6nzrzghcIp9CZ7ANHrI+QLRM/csz+AGA | |||
| szSp6S4mc1lnxxfbOhPPpebZPn0nIAXoZnnoVKdrxBVedPQHT59ZFvKTQ9Fs7gd3 | |||
| sYNuc7tJGFGC2CxBH4ANDpOQkc5q9JJ1HSGrXU3juxIiRgfA26Q22S9c71dXjElw | |||
| Ri584QH+bL6kkYmm8xpKF6TVwhwu5xx/jBPrbWqFrtbvLNrnfPoapTihBfdIhkT6 | |||
| nmgawbBHA02D5xEqB5SU3WJu | |||
| =eJNx | |||
| -----END PGP PUBLIC KEY BLOCK----- | |||
| ``` | |||
| @@ -0,0 +1,119 @@ | |||
| { | |||
| "name": "ramsey/collection", | |||
| "description": "A PHP library for representing and manipulating collections.", | |||
| "license": "MIT", | |||
| "type": "library", | |||
| "keywords": [ | |||
| "array", | |||
| "collection", | |||
| "hash", | |||
| "map", | |||
| "queue", | |||
| "set" | |||
| ], | |||
| "authors": [ | |||
| { | |||
| "name": "Ben Ramsey", | |||
| "email": "ben@benramsey.com", | |||
| "homepage": "https://benramsey.com" | |||
| } | |||
| ], | |||
| "require": { | |||
| "php": "^8.1" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "ergebnis/composer-normalize": "^2.28.3", | |||
| "fakerphp/faker": "^1.21", | |||
| "hamcrest/hamcrest-php": "^2.0", | |||
| "jangregor/phpstan-prophecy": "^1.0", | |||
| "mockery/mockery": "^1.5", | |||
| "php-parallel-lint/php-console-highlighter": "^1.0", | |||
| "php-parallel-lint/php-parallel-lint": "^1.3", | |||
| "phpcsstandards/phpcsutils": "^1.0.0-rc1", | |||
| "phpspec/prophecy-phpunit": "^2.0", | |||
| "phpstan/extension-installer": "^1.2", | |||
| "phpstan/phpstan": "^1.9", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.3", | |||
| "phpunit/phpunit": "^9.5", | |||
| "psalm/plugin-mockery": "^1.1", | |||
| "psalm/plugin-phpunit": "^0.18.4", | |||
| "ramsey/coding-standard": "^2.0.3", | |||
| "ramsey/conventional-commits": "^1.3", | |||
| "vimeo/psalm": "^5.4" | |||
| }, | |||
| "minimum-stability": "RC", | |||
| "prefer-stable": true, | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Ramsey\\Collection\\": "src/" | |||
| } | |||
| }, | |||
| "autoload-dev": { | |||
| "psr-4": { | |||
| "Ramsey\\Collection\\Test\\": "tests/", | |||
| "Ramsey\\Test\\Generics\\": "tests/generics/" | |||
| }, | |||
| "files": [ | |||
| "vendor/hamcrest/hamcrest-php/hamcrest/Hamcrest.php" | |||
| ] | |||
| }, | |||
| "config": { | |||
| "allow-plugins": { | |||
| "dealerdirect/phpcodesniffer-composer-installer": true, | |||
| "ergebnis/composer-normalize": true, | |||
| "phpstan/extension-installer": true, | |||
| "captainhook/plugin-composer": true | |||
| }, | |||
| "sort-packages": true | |||
| }, | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| }, | |||
| "ramsey/conventional-commits": { | |||
| "configFile": "conventional-commits.json" | |||
| } | |||
| }, | |||
| "scripts": { | |||
| "dev:analyze": [ | |||
| "@dev:analyze:phpstan", | |||
| "@dev:analyze:psalm" | |||
| ], | |||
| "dev:analyze:phpstan": "phpstan analyse --ansi --memory-limit=1G", | |||
| "dev:analyze:psalm": "psalm", | |||
| "dev:build:clean": "git clean -fX build/", | |||
| "dev:lint": [ | |||
| "@dev:lint:syntax", | |||
| "@dev:lint:style" | |||
| ], | |||
| "dev:lint:fix": "phpcbf", | |||
| "dev:lint:style": "phpcs --colors", | |||
| "dev:lint:syntax": "parallel-lint --colors src/ tests/", | |||
| "dev:test": [ | |||
| "@dev:lint", | |||
| "@dev:analyze", | |||
| "@dev:test:unit" | |||
| ], | |||
| "dev:test:coverage:ci": "phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", | |||
| "dev:test:coverage:html": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", | |||
| "dev:test:unit": "phpunit --colors=always", | |||
| "test": "@dev:test" | |||
| }, | |||
| "scripts-descriptions": { | |||
| "dev:analyze": "Runs all static analysis checks.", | |||
| "dev:analyze:phpstan": "Runs the PHPStan static analyzer.", | |||
| "dev:analyze:psalm": "Runs the Psalm static analyzer.", | |||
| "dev:build:clean": "Cleans the build/ directory.", | |||
| "dev:lint": "Runs all linting checks.", | |||
| "dev:lint:fix": "Auto-fixes coding standards issues, if possible.", | |||
| "dev:lint:style": "Checks for coding standards issues.", | |||
| "dev:lint:syntax": "Checks for syntax errors.", | |||
| "dev:test": "Runs linting, static analysis, and unit tests.", | |||
| "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.", | |||
| "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.", | |||
| "dev:test:unit": "Runs unit tests.", | |||
| "test": "Runs linting, static analysis, and unit tests." | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| { | |||
| "typeCase": "kebab", | |||
| "types": [ | |||
| "chore", | |||
| "ci", | |||
| "docs", | |||
| "feat", | |||
| "fix", | |||
| "refactor", | |||
| "security", | |||
| "style", | |||
| "test" | |||
| ], | |||
| "scopeCase": "kebab", | |||
| "scopeRequired": false, | |||
| "scopes": [], | |||
| "descriptionCase": null, | |||
| "descriptionEndMark": "", | |||
| "bodyRequired": false, | |||
| "bodyWrapWidth": 72, | |||
| "requiredFooters": [] | |||
| } | |||
| @@ -0,0 +1,171 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use ArrayIterator; | |||
| use Traversable; | |||
| use function count; | |||
| /** | |||
| * This class provides a basic implementation of `ArrayInterface`, to minimize | |||
| * the effort required to implement this interface. | |||
| * | |||
| * @template T | |||
| * @implements ArrayInterface<T> | |||
| */ | |||
| abstract class AbstractArray implements ArrayInterface | |||
| { | |||
| /** | |||
| * The items of this array. | |||
| * | |||
| * @var array<array-key, T> | |||
| */ | |||
| protected array $data = []; | |||
| /** | |||
| * Constructs a new array object. | |||
| * | |||
| * @param array<array-key, T> $data The initial items to add to this array. | |||
| */ | |||
| public function __construct(array $data = []) | |||
| { | |||
| // Invoke offsetSet() for each value added; in this way, sub-classes | |||
| // may provide additional logic about values added to the array object. | |||
| foreach ($data as $key => $value) { | |||
| $this[$key] = $value; | |||
| } | |||
| } | |||
| /** | |||
| * Returns an iterator for this array. | |||
| * | |||
| * @link http://php.net/manual/en/iteratoraggregate.getiterator.php IteratorAggregate::getIterator() | |||
| * | |||
| * @return Traversable<array-key, T> | |||
| */ | |||
| public function getIterator(): Traversable | |||
| { | |||
| return new ArrayIterator($this->data); | |||
| } | |||
| /** | |||
| * Returns `true` if the given offset exists in this array. | |||
| * | |||
| * @link http://php.net/manual/en/arrayaccess.offsetexists.php ArrayAccess::offsetExists() | |||
| * | |||
| * @param array-key $offset The offset to check. | |||
| */ | |||
| public function offsetExists(mixed $offset): bool | |||
| { | |||
| return isset($this->data[$offset]); | |||
| } | |||
| /** | |||
| * Returns the value at the specified offset. | |||
| * | |||
| * @link http://php.net/manual/en/arrayaccess.offsetget.php ArrayAccess::offsetGet() | |||
| * | |||
| * @param array-key $offset The offset for which a value should be returned. | |||
| * | |||
| * @return T the value stored at the offset, or null if the offset | |||
| * does not exist. | |||
| */ | |||
| public function offsetGet(mixed $offset): mixed | |||
| { | |||
| return $this->data[$offset]; | |||
| } | |||
| /** | |||
| * Sets the given value to the given offset in the array. | |||
| * | |||
| * @link http://php.net/manual/en/arrayaccess.offsetset.php ArrayAccess::offsetSet() | |||
| * | |||
| * @param array-key | null $offset The offset to set. If `null`, the value | |||
| * may be set at a numerically-indexed offset. | |||
| * @param T $value The value to set at the given offset. | |||
| */ | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($offset === null) { | |||
| $this->data[] = $value; | |||
| } else { | |||
| $this->data[$offset] = $value; | |||
| } | |||
| } | |||
| /** | |||
| * Removes the given offset and its value from the array. | |||
| * | |||
| * @link http://php.net/manual/en/arrayaccess.offsetunset.php ArrayAccess::offsetUnset() | |||
| * | |||
| * @param array-key $offset The offset to remove from the array. | |||
| */ | |||
| public function offsetUnset(mixed $offset): void | |||
| { | |||
| unset($this->data[$offset]); | |||
| } | |||
| /** | |||
| * Returns data suitable for PHP serialization. | |||
| * | |||
| * @link https://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.serialize | |||
| * @link https://www.php.net/serialize | |||
| * | |||
| * @return array<array-key, T> | |||
| */ | |||
| public function __serialize(): array | |||
| { | |||
| return $this->data; | |||
| } | |||
| /** | |||
| * Adds unserialized data to the object. | |||
| * | |||
| * @param array<array-key, T> $data | |||
| */ | |||
| public function __unserialize(array $data): void | |||
| { | |||
| $this->data = $data; | |||
| } | |||
| /** | |||
| * Returns the number of items in this array. | |||
| * | |||
| * @link http://php.net/manual/en/countable.count.php Countable::count() | |||
| */ | |||
| public function count(): int | |||
| { | |||
| return count($this->data); | |||
| } | |||
| public function clear(): void | |||
| { | |||
| $this->data = []; | |||
| } | |||
| /** | |||
| * @inheritDoc | |||
| */ | |||
| public function toArray(): array | |||
| { | |||
| return $this->data; | |||
| } | |||
| public function isEmpty(): bool | |||
| { | |||
| return $this->data === []; | |||
| } | |||
| } | |||
| @@ -0,0 +1,393 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Closure; | |||
| use Ramsey\Collection\Exception\CollectionMismatchException; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Exception\InvalidPropertyOrMethod; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use Ramsey\Collection\Exception\UnsupportedOperationException; | |||
| use Ramsey\Collection\Tool\TypeTrait; | |||
| use Ramsey\Collection\Tool\ValueExtractorTrait; | |||
| use Ramsey\Collection\Tool\ValueToStringTrait; | |||
| use function array_filter; | |||
| use function array_key_first; | |||
| use function array_key_last; | |||
| use function array_map; | |||
| use function array_merge; | |||
| use function array_reduce; | |||
| use function array_search; | |||
| use function array_udiff; | |||
| use function array_uintersect; | |||
| use function in_array; | |||
| use function is_int; | |||
| use function is_object; | |||
| use function spl_object_id; | |||
| use function sprintf; | |||
| use function usort; | |||
| /** | |||
| * This class provides a basic implementation of `CollectionInterface`, to | |||
| * minimize the effort required to implement this interface | |||
| * | |||
| * @template T | |||
| * @extends AbstractArray<T> | |||
| * @implements CollectionInterface<T> | |||
| */ | |||
| abstract class AbstractCollection extends AbstractArray implements CollectionInterface | |||
| { | |||
| use TypeTrait; | |||
| use ValueToStringTrait; | |||
| use ValueExtractorTrait; | |||
| /** | |||
| * @throws InvalidArgumentException if $element is of the wrong type. | |||
| */ | |||
| public function add(mixed $element): bool | |||
| { | |||
| $this[] = $element; | |||
| return true; | |||
| } | |||
| public function contains(mixed $element, bool $strict = true): bool | |||
| { | |||
| return in_array($element, $this->data, $strict); | |||
| } | |||
| /** | |||
| * @throws InvalidArgumentException if $element is of the wrong type. | |||
| */ | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($this->checkType($this->getType(), $value) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Value must be of type ' . $this->getType() . '; value is ' | |||
| . $this->toolValueToString($value), | |||
| ); | |||
| } | |||
| if ($offset === null) { | |||
| $this->data[] = $value; | |||
| } else { | |||
| $this->data[$offset] = $value; | |||
| } | |||
| } | |||
| public function remove(mixed $element): bool | |||
| { | |||
| if (($position = array_search($element, $this->data, true)) !== false) { | |||
| unset($this[$position]); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call column() on this | |||
| * collection. | |||
| * | |||
| * @inheritDoc | |||
| */ | |||
| public function column(string $propertyOrMethod): array | |||
| { | |||
| $temp = []; | |||
| foreach ($this->data as $item) { | |||
| /** @psalm-suppress MixedAssignment */ | |||
| $temp[] = $this->extractValue($item, $propertyOrMethod); | |||
| } | |||
| return $temp; | |||
| } | |||
| /** | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this collection is empty. | |||
| */ | |||
| public function first(): mixed | |||
| { | |||
| $firstIndex = array_key_first($this->data); | |||
| if ($firstIndex === null) { | |||
| throw new NoSuchElementException('Can\'t determine first item. Collection is empty'); | |||
| } | |||
| return $this->data[$firstIndex]; | |||
| } | |||
| /** | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this collection is empty. | |||
| */ | |||
| public function last(): mixed | |||
| { | |||
| $lastIndex = array_key_last($this->data); | |||
| if ($lastIndex === null) { | |||
| throw new NoSuchElementException('Can\'t determine last item. Collection is empty'); | |||
| } | |||
| return $this->data[$lastIndex]; | |||
| } | |||
| /** | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call sort() on this | |||
| * collection. | |||
| */ | |||
| public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): CollectionInterface | |||
| { | |||
| $collection = clone $this; | |||
| usort( | |||
| $collection->data, | |||
| /** | |||
| * @param T $a | |||
| * @param T $b | |||
| */ | |||
| function (mixed $a, mixed $b) use ($propertyOrMethod, $order): int { | |||
| /** @var mixed $aValue */ | |||
| $aValue = $this->extractValue($a, $propertyOrMethod); | |||
| /** @var mixed $bValue */ | |||
| $bValue = $this->extractValue($b, $propertyOrMethod); | |||
| return ($aValue <=> $bValue) * ($order === Sort::Descending ? -1 : 1); | |||
| }, | |||
| ); | |||
| return $collection; | |||
| } | |||
| /** | |||
| * @param callable(T): bool $callback A callable to use for filtering elements. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| */ | |||
| public function filter(callable $callback): CollectionInterface | |||
| { | |||
| $collection = clone $this; | |||
| $collection->data = array_merge([], array_filter($collection->data, $callback)); | |||
| return $collection; | |||
| } | |||
| /** | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call where() on this | |||
| * collection. | |||
| */ | |||
| public function where(?string $propertyOrMethod, mixed $value): CollectionInterface | |||
| { | |||
| return $this->filter( | |||
| /** | |||
| * @param T $item | |||
| */ | |||
| function (mixed $item) use ($propertyOrMethod, $value): bool { | |||
| /** @var mixed $accessorValue */ | |||
| $accessorValue = $this->extractValue($item, $propertyOrMethod); | |||
| return $accessorValue === $value; | |||
| }, | |||
| ); | |||
| } | |||
| /** | |||
| * @param callable(T): TCallbackReturn $callback A callable to apply to each | |||
| * item of the collection. | |||
| * | |||
| * @return CollectionInterface<TCallbackReturn> | |||
| * | |||
| * @template TCallbackReturn | |||
| */ | |||
| public function map(callable $callback): CollectionInterface | |||
| { | |||
| /** @var Collection<TCallbackReturn> */ | |||
| return new Collection('mixed', array_map($callback, $this->data)); | |||
| } | |||
| /** | |||
| * @param callable(TCarry, T): TCarry $callback A callable to apply to each | |||
| * item of the collection to reduce it to a single value. | |||
| * @param TCarry $initial This is the initial value provided to the callback. | |||
| * | |||
| * @return TCarry | |||
| * | |||
| * @template TCarry | |||
| */ | |||
| public function reduce(callable $callback, mixed $initial): mixed | |||
| { | |||
| /** @var TCarry */ | |||
| return array_reduce($this->data, $callback, $initial); | |||
| } | |||
| /** | |||
| * @param CollectionInterface<T> $other The collection to check for divergent | |||
| * items. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if the compared collections are of | |||
| * differing types. | |||
| */ | |||
| public function diff(CollectionInterface $other): CollectionInterface | |||
| { | |||
| $this->compareCollectionTypes($other); | |||
| $diffAtoB = array_udiff($this->data, $other->toArray(), $this->getComparator()); | |||
| $diffBtoA = array_udiff($other->toArray(), $this->data, $this->getComparator()); | |||
| /** @var array<array-key, T> $diff */ | |||
| $diff = array_merge($diffAtoB, $diffBtoA); | |||
| $collection = clone $this; | |||
| $collection->data = $diff; | |||
| return $collection; | |||
| } | |||
| /** | |||
| * @param CollectionInterface<T> $other The collection to check for | |||
| * intersecting items. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if the compared collections are of | |||
| * differing types. | |||
| */ | |||
| public function intersect(CollectionInterface $other): CollectionInterface | |||
| { | |||
| $this->compareCollectionTypes($other); | |||
| /** @var array<array-key, T> $intersect */ | |||
| $intersect = array_uintersect($this->data, $other->toArray(), $this->getComparator()); | |||
| $collection = clone $this; | |||
| $collection->data = $intersect; | |||
| return $collection; | |||
| } | |||
| /** | |||
| * @param CollectionInterface<T> ...$collections The collections to merge. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if unable to merge any of the given | |||
| * collections or items within the given collections due to type | |||
| * mismatch errors. | |||
| */ | |||
| public function merge(CollectionInterface ...$collections): CollectionInterface | |||
| { | |||
| $mergedCollection = clone $this; | |||
| foreach ($collections as $index => $collection) { | |||
| if (!$collection instanceof static) { | |||
| throw new CollectionMismatchException( | |||
| sprintf('Collection with index %d must be of type %s', $index, static::class), | |||
| ); | |||
| } | |||
| // When using generics (Collection.php, Set.php, etc), | |||
| // we also need to make sure that the internal types match each other | |||
| if ($this->getUniformType($collection) !== $this->getUniformType($this)) { | |||
| throw new CollectionMismatchException( | |||
| sprintf( | |||
| 'Collection items in collection with index %d must be of type %s', | |||
| $index, | |||
| $this->getType(), | |||
| ), | |||
| ); | |||
| } | |||
| foreach ($collection as $key => $value) { | |||
| if (is_int($key)) { | |||
| $mergedCollection[] = $value; | |||
| } else { | |||
| $mergedCollection[$key] = $value; | |||
| } | |||
| } | |||
| } | |||
| return $mergedCollection; | |||
| } | |||
| /** | |||
| * @param CollectionInterface<T> $other | |||
| * | |||
| * @throws CollectionMismatchException | |||
| */ | |||
| private function compareCollectionTypes(CollectionInterface $other): void | |||
| { | |||
| if (!$other instanceof static) { | |||
| throw new CollectionMismatchException('Collection must be of type ' . static::class); | |||
| } | |||
| // When using generics (Collection.php, Set.php, etc), | |||
| // we also need to make sure that the internal types match each other | |||
| if ($this->getUniformType($other) !== $this->getUniformType($this)) { | |||
| throw new CollectionMismatchException('Collection items must be of type ' . $this->getType()); | |||
| } | |||
| } | |||
| private function getComparator(): Closure | |||
| { | |||
| return /** | |||
| * @param T $a | |||
| * @param T $b | |||
| */ | |||
| function (mixed $a, mixed $b): int { | |||
| // If the two values are object, we convert them to unique scalars. | |||
| // If the collection contains mixed values (unlikely) where some are objects | |||
| // and some are not, we leave them as they are. | |||
| // The comparator should still work and the result of $a < $b should | |||
| // be consistent but unpredictable since not documented. | |||
| if (is_object($a) && is_object($b)) { | |||
| $a = spl_object_id($a); | |||
| $b = spl_object_id($b); | |||
| } | |||
| return $a === $b ? 0 : ($a < $b ? 1 : -1); | |||
| }; | |||
| } | |||
| /** | |||
| * @param CollectionInterface<mixed> $collection | |||
| */ | |||
| private function getUniformType(CollectionInterface $collection): string | |||
| { | |||
| return match ($collection->getType()) { | |||
| 'integer' => 'int', | |||
| 'boolean' => 'bool', | |||
| 'double' => 'float', | |||
| default => $collection->getType(), | |||
| }; | |||
| } | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| /** | |||
| * This class contains the basic implementation of a collection that does not | |||
| * allow duplicated values (a set), to minimize the effort required to implement | |||
| * this specific type of collection. | |||
| * | |||
| * @template T | |||
| * @extends AbstractCollection<T> | |||
| */ | |||
| abstract class AbstractSet extends AbstractCollection | |||
| { | |||
| public function add(mixed $element): bool | |||
| { | |||
| if ($this->contains($element)) { | |||
| return false; | |||
| } | |||
| return parent::add($element); | |||
| } | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($this->contains($value)) { | |||
| return; | |||
| } | |||
| parent::offsetSet($offset, $value); | |||
| } | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use ArrayAccess; | |||
| use Countable; | |||
| use IteratorAggregate; | |||
| /** | |||
| * `ArrayInterface` provides traversable array functionality to data types. | |||
| * | |||
| * @template T | |||
| * @extends ArrayAccess<array-key, T> | |||
| * @extends IteratorAggregate<array-key, T> | |||
| */ | |||
| interface ArrayInterface extends | |||
| ArrayAccess, | |||
| Countable, | |||
| IteratorAggregate | |||
| { | |||
| /** | |||
| * Removes all items from this array. | |||
| */ | |||
| public function clear(): void; | |||
| /** | |||
| * Returns a native PHP array representation of this array object. | |||
| * | |||
| * @return array<array-key, T> | |||
| */ | |||
| public function toArray(): array; | |||
| /** | |||
| * Returns `true` if this array is empty. | |||
| */ | |||
| public function isEmpty(): bool; | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| /** | |||
| * A collection represents a group of objects. | |||
| * | |||
| * Each object in the collection is of a specific, defined type. | |||
| * | |||
| * This is a direct implementation of `CollectionInterface`, provided for | |||
| * the sake of convenience. | |||
| * | |||
| * Example usage: | |||
| * | |||
| * ``` php | |||
| * $collection = new \Ramsey\Collection\Collection('My\\Foo'); | |||
| * $collection->add(new \My\Foo()); | |||
| * $collection->add(new \My\Foo()); | |||
| * | |||
| * foreach ($collection as $foo) { | |||
| * // Do something with $foo | |||
| * } | |||
| * ``` | |||
| * | |||
| * It is preferable to subclass `AbstractCollection` to create your own typed | |||
| * collections. For example: | |||
| * | |||
| * ``` php | |||
| * namespace My\Foo; | |||
| * | |||
| * class FooCollection extends \Ramsey\Collection\AbstractCollection | |||
| * { | |||
| * public function getType() | |||
| * { | |||
| * return 'My\\Foo'; | |||
| * } | |||
| * } | |||
| * ``` | |||
| * | |||
| * And then use it similarly to the earlier example: | |||
| * | |||
| * ``` php | |||
| * $fooCollection = new \My\Foo\FooCollection(); | |||
| * $fooCollection->add(new \My\Foo()); | |||
| * $fooCollection->add(new \My\Foo()); | |||
| * | |||
| * foreach ($fooCollection as $foo) { | |||
| * // Do something with $foo | |||
| * } | |||
| * ``` | |||
| * | |||
| * The benefit with this approach is that you may do type-checking on the | |||
| * collection object: | |||
| * | |||
| * ``` php | |||
| * if ($collection instanceof \My\Foo\FooCollection) { | |||
| * // the collection is a collection of My\Foo objects | |||
| * } | |||
| * ``` | |||
| * | |||
| * @template T | |||
| * @extends AbstractCollection<T> | |||
| */ | |||
| class Collection extends AbstractCollection | |||
| { | |||
| /** | |||
| * Constructs a collection object of the specified type, optionally with the | |||
| * specified data. | |||
| * | |||
| * @param string $collectionType The type or class name associated with this | |||
| * collection. | |||
| * @param array<array-key, T> $data The initial items to store in the collection. | |||
| */ | |||
| public function __construct(private readonly string $collectionType, array $data = []) | |||
| { | |||
| parent::__construct($data); | |||
| } | |||
| public function getType(): string | |||
| { | |||
| return $this->collectionType; | |||
| } | |||
| } | |||
| @@ -0,0 +1,253 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Ramsey\Collection\Exception\CollectionMismatchException; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Exception\InvalidPropertyOrMethod; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use Ramsey\Collection\Exception\UnsupportedOperationException; | |||
| /** | |||
| * A collection represents a group of values, known as its elements. | |||
| * | |||
| * Some collections allow duplicate elements and others do not. Some are ordered | |||
| * and others unordered. | |||
| * | |||
| * @template T | |||
| * @extends ArrayInterface<T> | |||
| */ | |||
| interface CollectionInterface extends ArrayInterface | |||
| { | |||
| /** | |||
| * Ensures that this collection contains the specified element (optional | |||
| * operation). | |||
| * | |||
| * Returns `true` if this collection changed as a result of the call. | |||
| * (Returns `false` if this collection does not permit duplicates and | |||
| * already contains the specified element.) | |||
| * | |||
| * Collections that support this operation may place limitations on what | |||
| * elements may be added to this collection. In particular, some | |||
| * collections will refuse to add `null` elements, and others will impose | |||
| * restrictions on the type of elements that may be added. Collection | |||
| * classes should clearly specify in their documentation any restrictions | |||
| * on what elements may be added. | |||
| * | |||
| * If a collection refuses to add a particular element for any reason other | |||
| * than that it already contains the element, it must throw an exception | |||
| * (rather than returning `false`). This preserves the invariant that a | |||
| * collection always contains the specified element after this call returns. | |||
| * | |||
| * @param T $element The element to add to the collection. | |||
| * | |||
| * @return bool `true` if this collection changed as a result of the call. | |||
| * | |||
| * @throws InvalidArgumentException if the collection refuses to add the | |||
| * $element for any reason other than that it already contains the element. | |||
| */ | |||
| public function add(mixed $element): bool; | |||
| /** | |||
| * Returns `true` if this collection contains the specified element. | |||
| * | |||
| * @param T $element The element to check whether the collection contains. | |||
| * @param bool $strict Whether to perform a strict type check on the value. | |||
| */ | |||
| public function contains(mixed $element, bool $strict = true): bool; | |||
| /** | |||
| * Returns the type associated with this collection. | |||
| */ | |||
| public function getType(): string; | |||
| /** | |||
| * Removes a single instance of the specified element from this collection, | |||
| * if it is present. | |||
| * | |||
| * @param T $element The element to remove from the collection. | |||
| * | |||
| * @return bool `true` if an element was removed as a result of this call. | |||
| */ | |||
| public function remove(mixed $element): bool; | |||
| /** | |||
| * Returns the values from the given property, method, or array key. | |||
| * | |||
| * @param string $propertyOrMethod The name of the property, method, or | |||
| * array key to evaluate and return. | |||
| * | |||
| * @return array<int, mixed> | |||
| * | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call column() on this | |||
| * collection. | |||
| */ | |||
| public function column(string $propertyOrMethod): array; | |||
| /** | |||
| * Returns the first item of the collection. | |||
| * | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this collection is empty. | |||
| */ | |||
| public function first(): mixed; | |||
| /** | |||
| * Returns the last item of the collection. | |||
| * | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this collection is empty. | |||
| */ | |||
| public function last(): mixed; | |||
| /** | |||
| * Sort the collection by a property, method, or array key with the given | |||
| * sort order. | |||
| * | |||
| * If $propertyOrMethod is `null`, this will sort by comparing each element. | |||
| * | |||
| * This will always leave the original collection untouched and will return | |||
| * a new one. | |||
| * | |||
| * @param string | null $propertyOrMethod The property, method, or array key | |||
| * to sort by. | |||
| * @param Sort $order The sort order for the resulting collection. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call sort() on this | |||
| * collection. | |||
| */ | |||
| public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): self; | |||
| /** | |||
| * Filter out items of the collection which don't match the criteria of | |||
| * given callback. | |||
| * | |||
| * This will always leave the original collection untouched and will return | |||
| * a new one. | |||
| * | |||
| * See the {@link http://php.net/manual/en/function.array-filter.php PHP array_filter() documentation} | |||
| * for examples of how the `$callback` parameter works. | |||
| * | |||
| * @param callable(T): bool $callback A callable to use for filtering elements. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| */ | |||
| public function filter(callable $callback): self; | |||
| /** | |||
| * Create a new collection where the result of the given property, method, | |||
| * or array key of each item in the collection equals the given value. | |||
| * | |||
| * This will always leave the original collection untouched and will return | |||
| * a new one. | |||
| * | |||
| * @param string | null $propertyOrMethod The property, method, or array key | |||
| * to evaluate. If `null`, the element itself is compared to $value. | |||
| * @param mixed $value The value to match. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist | |||
| * on the elements in this collection. | |||
| * @throws UnsupportedOperationException if unable to call where() on this | |||
| * collection. | |||
| */ | |||
| public function where(?string $propertyOrMethod, mixed $value): self; | |||
| /** | |||
| * Apply a given callback method on each item of the collection. | |||
| * | |||
| * This will always leave the original collection untouched. The new | |||
| * collection is created by mapping the callback to each item of the | |||
| * original collection. | |||
| * | |||
| * See the {@link http://php.net/manual/en/function.array-map.php PHP array_map() documentation} | |||
| * for examples of how the `$callback` parameter works. | |||
| * | |||
| * @param callable(T): TCallbackReturn $callback A callable to apply to each | |||
| * item of the collection. | |||
| * | |||
| * @return CollectionInterface<TCallbackReturn> | |||
| * | |||
| * @template TCallbackReturn | |||
| */ | |||
| public function map(callable $callback): self; | |||
| /** | |||
| * Apply a given callback method on each item of the collection | |||
| * to reduce it to a single value. | |||
| * | |||
| * See the {@link http://php.net/manual/en/function.array-reduce.php PHP array_reduce() documentation} | |||
| * for examples of how the `$callback` and `$initial` parameters work. | |||
| * | |||
| * @param callable(TCarry, T): TCarry $callback A callable to apply to each | |||
| * item of the collection to reduce it to a single value. | |||
| * @param TCarry $initial This is the initial value provided to the callback. | |||
| * | |||
| * @return TCarry | |||
| * | |||
| * @template TCarry | |||
| */ | |||
| public function reduce(callable $callback, mixed $initial): mixed; | |||
| /** | |||
| * Create a new collection with divergent items between current and given | |||
| * collection. | |||
| * | |||
| * @param CollectionInterface<T> $other The collection to check for divergent | |||
| * items. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if the compared collections are of | |||
| * differing types. | |||
| */ | |||
| public function diff(CollectionInterface $other): self; | |||
| /** | |||
| * Create a new collection with intersecting item between current and given | |||
| * collection. | |||
| * | |||
| * @param CollectionInterface<T> $other The collection to check for | |||
| * intersecting items. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if the compared collections are of | |||
| * differing types. | |||
| */ | |||
| public function intersect(CollectionInterface $other): self; | |||
| /** | |||
| * Merge current items and items of given collections into a new one. | |||
| * | |||
| * @param CollectionInterface<T> ...$collections The collections to merge. | |||
| * | |||
| * @return CollectionInterface<T> | |||
| * | |||
| * @throws CollectionMismatchException if unable to merge any of the given | |||
| * collections or items within the given collections due to type | |||
| * mismatch errors. | |||
| */ | |||
| public function merge(CollectionInterface ...$collections): self; | |||
| } | |||
| @@ -0,0 +1,166 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use function array_key_last; | |||
| use function array_pop; | |||
| use function array_unshift; | |||
| /** | |||
| * This class provides a basic implementation of `DoubleEndedQueueInterface`, to | |||
| * minimize the effort required to implement this interface. | |||
| * | |||
| * @template T | |||
| * @extends Queue<T> | |||
| * @implements DoubleEndedQueueInterface<T> | |||
| */ | |||
| class DoubleEndedQueue extends Queue implements DoubleEndedQueueInterface | |||
| { | |||
| /** | |||
| * Constructs a double-ended queue (dequeue) object of the specified type, | |||
| * optionally with the specified data. | |||
| * | |||
| * @param string $queueType The type or class name associated with this dequeue. | |||
| * @param array<array-key, T> $data The initial items to store in the dequeue. | |||
| */ | |||
| public function __construct(private readonly string $queueType, array $data = []) | |||
| { | |||
| parent::__construct($this->queueType, $data); | |||
| } | |||
| /** | |||
| * @throws InvalidArgumentException if $element is of the wrong type | |||
| */ | |||
| public function addFirst(mixed $element): bool | |||
| { | |||
| if ($this->checkType($this->getType(), $element) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Value must be of type ' . $this->getType() . '; value is ' | |||
| . $this->toolValueToString($element), | |||
| ); | |||
| } | |||
| array_unshift($this->data, $element); | |||
| return true; | |||
| } | |||
| /** | |||
| * @throws InvalidArgumentException if $element is of the wrong type | |||
| */ | |||
| public function addLast(mixed $element): bool | |||
| { | |||
| return $this->add($element); | |||
| } | |||
| public function offerFirst(mixed $element): bool | |||
| { | |||
| try { | |||
| return $this->addFirst($element); | |||
| } catch (InvalidArgumentException) { | |||
| return false; | |||
| } | |||
| } | |||
| public function offerLast(mixed $element): bool | |||
| { | |||
| return $this->offer($element); | |||
| } | |||
| /** | |||
| * @return T the first element in this queue. | |||
| * | |||
| * @throws NoSuchElementException if the queue is empty | |||
| */ | |||
| public function removeFirst(): mixed | |||
| { | |||
| return $this->remove(); | |||
| } | |||
| /** | |||
| * @return T the last element in this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function removeLast(): mixed | |||
| { | |||
| return $this->pollLast() ?? throw new NoSuchElementException( | |||
| 'Can\'t return element from Queue. Queue is empty.', | |||
| ); | |||
| } | |||
| /** | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function pollFirst(): mixed | |||
| { | |||
| return $this->poll(); | |||
| } | |||
| /** | |||
| * @return T | null the tail of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function pollLast(): mixed | |||
| { | |||
| return array_pop($this->data); | |||
| } | |||
| /** | |||
| * @return T the head of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function firstElement(): mixed | |||
| { | |||
| return $this->element(); | |||
| } | |||
| /** | |||
| * @return T the tail of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function lastElement(): mixed | |||
| { | |||
| return $this->peekLast() ?? throw new NoSuchElementException( | |||
| 'Can\'t return element from Queue. Queue is empty.', | |||
| ); | |||
| } | |||
| /** | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function peekFirst(): mixed | |||
| { | |||
| return $this->peek(); | |||
| } | |||
| /** | |||
| * @return T | null the tail of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function peekLast(): mixed | |||
| { | |||
| $lastIndex = array_key_last($this->data); | |||
| if ($lastIndex === null) { | |||
| return null; | |||
| } | |||
| return $this->data[$lastIndex]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,313 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use RuntimeException; | |||
| /** | |||
| * A linear collection that supports element insertion and removal at both ends. | |||
| * | |||
| * Most `DoubleEndedQueueInterface` implementations place no fixed limits on the | |||
| * number of elements they may contain, but this interface supports | |||
| * capacity-restricted double-ended queues as well as those with no fixed size | |||
| * limit. | |||
| * | |||
| * This interface defines methods to access the elements at both ends of the | |||
| * double-ended queue. Methods are provided to insert, remove, and examine the | |||
| * element. Each of these methods exists in two forms: one throws an exception | |||
| * if the operation fails, the other returns a special value (either `null` or | |||
| * `false`, depending on the operation). The latter form of the insert operation | |||
| * is designed specifically for use with capacity-restricted implementations; in | |||
| * most implementations, insert operations cannot fail. | |||
| * | |||
| * The twelve methods described above are summarized in the following table: | |||
| * | |||
| * <table> | |||
| * <caption>Summary of DoubleEndedQueueInterface methods</caption> | |||
| * <thead> | |||
| * <tr> | |||
| * <th></th> | |||
| * <th colspan=2>First Element (Head)</th> | |||
| * <th colspan=2>Last Element (Tail)</th> | |||
| * </tr> | |||
| * <tr> | |||
| * <td></td> | |||
| * <td><em>Throws exception</em></td> | |||
| * <td><em>Special value</em></td> | |||
| * <td><em>Throws exception</em></td> | |||
| * <td><em>Special value</em></td> | |||
| * </tr> | |||
| * </thead> | |||
| * <tbody> | |||
| * <tr> | |||
| * <th>Insert</th> | |||
| * <td><code>addFirst()</code></td> | |||
| * <td><code>offerFirst()</code></td> | |||
| * <td><code>addLast()</code></td> | |||
| * <td><code>offerLast()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <th>Remove</th> | |||
| * <td><code>removeFirst()</code></td> | |||
| * <td><code>pollFirst()</code></td> | |||
| * <td><code>removeLast()</code></td> | |||
| * <td><code>pollLast()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <th>Examine</th> | |||
| * <td><code>firstElement()</code></td> | |||
| * <td><code>peekFirst()</code></td> | |||
| * <td><code>lastElement()</code></td> | |||
| * <td><code>peekLast()</code></td> | |||
| * </tr> | |||
| * </tbody> | |||
| * </table> | |||
| * | |||
| * This interface extends the `QueueInterface`. When a double-ended queue is | |||
| * used as a queue, FIFO (first-in-first-out) behavior results. Elements are | |||
| * added at the end of the double-ended queue and removed from the beginning. | |||
| * The methods inherited from the `QueueInterface` are precisely equivalent to | |||
| * `DoubleEndedQueueInterface` methods as indicated in the following table: | |||
| * | |||
| * <table> | |||
| * <caption>Comparison of QueueInterface and DoubleEndedQueueInterface methods</caption> | |||
| * <thead> | |||
| * <tr> | |||
| * <th>QueueInterface Method</th> | |||
| * <th>DoubleEndedQueueInterface Method</th> | |||
| * </tr> | |||
| * </thead> | |||
| * <tbody> | |||
| * <tr> | |||
| * <td><code>add()</code></td> | |||
| * <td><code>addLast()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><code>offer()</code></td> | |||
| * <td><code>offerLast()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><code>remove()</code></td> | |||
| * <td><code>removeFirst()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><code>poll()</code></td> | |||
| * <td><code>pollFirst()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><code>element()</code></td> | |||
| * <td><code>firstElement()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><code>peek()</code></td> | |||
| * <td><code>peekFirst()</code></td> | |||
| * </tr> | |||
| * </tbody> | |||
| * </table> | |||
| * | |||
| * Double-ended queues can also be used as LIFO (last-in-first-out) stacks. When | |||
| * a double-ended queue is used as a stack, elements are pushed and popped from | |||
| * the beginning of the double-ended queue. Stack concepts are precisely | |||
| * equivalent to `DoubleEndedQueueInterface` methods as indicated in the table | |||
| * below: | |||
| * | |||
| * <table> | |||
| * <caption>Comparison of stack concepts and DoubleEndedQueueInterface methods</caption> | |||
| * <thead> | |||
| * <tr> | |||
| * <th>Stack concept</th> | |||
| * <th>DoubleEndedQueueInterface Method</th> | |||
| * </tr> | |||
| * </thead> | |||
| * <tbody> | |||
| * <tr> | |||
| * <td><em>push</em></td> | |||
| * <td><code>addFirst()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><em>pop</em></td> | |||
| * <td><code>removeFirst()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <td><em>peek</em></td> | |||
| * <td><code>peekFirst()</code></td> | |||
| * </tr> | |||
| * </tbody> | |||
| * </table> | |||
| * | |||
| * Note that the `peek()` method works equally well when a double-ended queue is | |||
| * used as a queue or a stack; in either case, elements are drawn from the | |||
| * beginning of the double-ended queue. | |||
| * | |||
| * While `DoubleEndedQueueInterface` implementations are not strictly required | |||
| * to prohibit the insertion of `null` elements, they are strongly encouraged to | |||
| * do so. Users of any `DoubleEndedQueueInterface` implementations that do allow | |||
| * `null` elements are strongly encouraged *not* to take advantage of the | |||
| * ability to insert nulls. This is so because `null` is used as a special | |||
| * return value by various methods to indicated that the double-ended queue is | |||
| * empty. | |||
| * | |||
| * @template T | |||
| * @extends QueueInterface<T> | |||
| */ | |||
| interface DoubleEndedQueueInterface extends QueueInterface | |||
| { | |||
| /** | |||
| * Inserts the specified element at the front of this queue if it is | |||
| * possible to do so immediately without violating capacity restrictions. | |||
| * | |||
| * When using a capacity-restricted double-ended queue, it is generally | |||
| * preferable to use the `offerFirst()` method. | |||
| * | |||
| * @param T $element The element to add to the front of this queue. | |||
| * | |||
| * @return bool `true` if this queue changed as a result of the call. | |||
| * | |||
| * @throws RuntimeException if a queue refuses to add a particular element | |||
| * for any reason other than that it already contains the element. | |||
| * Implementations should use a more-specific exception that extends | |||
| * `\RuntimeException`. | |||
| */ | |||
| public function addFirst(mixed $element): bool; | |||
| /** | |||
| * Inserts the specified element at the end of this queue if it is possible | |||
| * to do so immediately without violating capacity restrictions. | |||
| * | |||
| * When using a capacity-restricted double-ended queue, it is generally | |||
| * preferable to use the `offerLast()` method. | |||
| * | |||
| * This method is equivalent to `add()`. | |||
| * | |||
| * @param T $element The element to add to the end of this queue. | |||
| * | |||
| * @return bool `true` if this queue changed as a result of the call. | |||
| * | |||
| * @throws RuntimeException if a queue refuses to add a particular element | |||
| * for any reason other than that it already contains the element. | |||
| * Implementations should use a more-specific exception that extends | |||
| * `\RuntimeException`. | |||
| */ | |||
| public function addLast(mixed $element): bool; | |||
| /** | |||
| * Inserts the specified element at the front of this queue if it is | |||
| * possible to do so immediately without violating capacity restrictions. | |||
| * | |||
| * When using a capacity-restricted queue, this method is generally | |||
| * preferable to `addFirst()`, which can fail to insert an element only by | |||
| * throwing an exception. | |||
| * | |||
| * @param T $element The element to add to the front of this queue. | |||
| * | |||
| * @return bool `true` if the element was added to this queue, else `false`. | |||
| */ | |||
| public function offerFirst(mixed $element): bool; | |||
| /** | |||
| * Inserts the specified element at the end of this queue if it is possible | |||
| * to do so immediately without violating capacity restrictions. | |||
| * | |||
| * When using a capacity-restricted queue, this method is generally | |||
| * preferable to `addLast()` which can fail to insert an element only by | |||
| * throwing an exception. | |||
| * | |||
| * @param T $element The element to add to the end of this queue. | |||
| * | |||
| * @return bool `true` if the element was added to this queue, else `false`. | |||
| */ | |||
| public function offerLast(mixed $element): bool; | |||
| /** | |||
| * Retrieves and removes the head of this queue. | |||
| * | |||
| * This method differs from `pollFirst()` only in that it throws an | |||
| * exception if this queue is empty. | |||
| * | |||
| * @return T the first element in this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function removeFirst(): mixed; | |||
| /** | |||
| * Retrieves and removes the tail of this queue. | |||
| * | |||
| * This method differs from `pollLast()` only in that it throws an exception | |||
| * if this queue is empty. | |||
| * | |||
| * @return T the last element in this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function removeLast(): mixed; | |||
| /** | |||
| * Retrieves and removes the head of this queue, or returns `null` if this | |||
| * queue is empty. | |||
| * | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function pollFirst(): mixed; | |||
| /** | |||
| * Retrieves and removes the tail of this queue, or returns `null` if this | |||
| * queue is empty. | |||
| * | |||
| * @return T | null the tail of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function pollLast(): mixed; | |||
| /** | |||
| * Retrieves, but does not remove, the head of this queue. | |||
| * | |||
| * This method differs from `peekFirst()` only in that it throws an | |||
| * exception if this queue is empty. | |||
| * | |||
| * @return T the head of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function firstElement(): mixed; | |||
| /** | |||
| * Retrieves, but does not remove, the tail of this queue. | |||
| * | |||
| * This method differs from `peekLast()` only in that it throws an exception | |||
| * if this queue is empty. | |||
| * | |||
| * @return T the tail of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function lastElement(): mixed; | |||
| /** | |||
| * Retrieves, but does not remove, the head of this queue, or returns `null` | |||
| * if this queue is empty. | |||
| * | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function peekFirst(): mixed; | |||
| /** | |||
| * Retrieves, but does not remove, the tail of this queue, or returns `null` | |||
| * if this queue is empty. | |||
| * | |||
| * @return T | null the tail of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function peekLast(): mixed; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use Throwable; | |||
| interface CollectionException extends Throwable | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use RuntimeException; | |||
| /** | |||
| * Thrown when attempting to operate on collections of differing types. | |||
| */ | |||
| class CollectionMismatchException extends RuntimeException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use InvalidArgumentException as PhpInvalidArgumentException; | |||
| /** | |||
| * Thrown to indicate an argument is not of the expected type. | |||
| */ | |||
| class InvalidArgumentException extends PhpInvalidArgumentException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use RuntimeException; | |||
| /** | |||
| * Thrown when attempting to evaluate a property, method, or array key | |||
| * that doesn't exist on an element or cannot otherwise be evaluated in the | |||
| * current context. | |||
| */ | |||
| class InvalidPropertyOrMethod extends RuntimeException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use RuntimeException; | |||
| /** | |||
| * Thrown when attempting to access an element that does not exist. | |||
| */ | |||
| class NoSuchElementException extends RuntimeException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use OutOfBoundsException as PhpOutOfBoundsException; | |||
| /** | |||
| * Thrown when attempting to access an element out of the range of the collection. | |||
| */ | |||
| class OutOfBoundsException extends PhpOutOfBoundsException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Exception; | |||
| use RuntimeException; | |||
| /** | |||
| * Thrown to indicate that the requested operation is not supported. | |||
| */ | |||
| class UnsupportedOperationException extends RuntimeException implements CollectionException | |||
| { | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| /** | |||
| * `GenericArray` represents a standard array object. | |||
| * | |||
| * @extends AbstractArray<mixed> | |||
| */ | |||
| class GenericArray extends AbstractArray | |||
| { | |||
| } | |||
| @@ -0,0 +1,203 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| use Ramsey\Collection\AbstractArray; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Traversable; | |||
| use function array_key_exists; | |||
| use function array_keys; | |||
| use function in_array; | |||
| use function var_export; | |||
| /** | |||
| * This class provides a basic implementation of `MapInterface`, to minimize the | |||
| * effort required to implement this interface. | |||
| * | |||
| * @template K of array-key | |||
| * @template T | |||
| * @extends AbstractArray<T> | |||
| * @implements MapInterface<K, T> | |||
| */ | |||
| abstract class AbstractMap extends AbstractArray implements MapInterface | |||
| { | |||
| /** | |||
| * @param array<K, T> $data The initial items to add to this map. | |||
| */ | |||
| public function __construct(array $data = []) | |||
| { | |||
| parent::__construct($data); | |||
| } | |||
| /** | |||
| * @return Traversable<K, T> | |||
| */ | |||
| public function getIterator(): Traversable | |||
| { | |||
| return parent::getIterator(); | |||
| } | |||
| /** | |||
| * @param K $offset The offset to set | |||
| * @param T $value The value to set at the given offset. | |||
| * | |||
| * @inheritDoc | |||
| * @psalm-suppress MoreSpecificImplementedParamType,DocblockTypeContradiction | |||
| */ | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($offset === null) { | |||
| throw new InvalidArgumentException( | |||
| 'Map elements are key/value pairs; a key must be provided for ' | |||
| . 'value ' . var_export($value, true), | |||
| ); | |||
| } | |||
| $this->data[$offset] = $value; | |||
| } | |||
| public function containsKey(int | string $key): bool | |||
| { | |||
| return array_key_exists($key, $this->data); | |||
| } | |||
| public function containsValue(mixed $value): bool | |||
| { | |||
| return in_array($value, $this->data, true); | |||
| } | |||
| /** | |||
| * @inheritDoc | |||
| */ | |||
| public function keys(): array | |||
| { | |||
| return array_keys($this->data); | |||
| } | |||
| /** | |||
| * @param K $key The key to return from the map. | |||
| * @param T | null $defaultValue The default value to use if `$key` is not found. | |||
| * | |||
| * @return T | null the value or `null` if the key could not be found. | |||
| */ | |||
| public function get(int | string $key, mixed $defaultValue = null): mixed | |||
| { | |||
| return $this[$key] ?? $defaultValue; | |||
| } | |||
| /** | |||
| * @param K $key The key to put or replace in the map. | |||
| * @param T $value The value to store at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function put(int | string $key, mixed $value): mixed | |||
| { | |||
| $previousValue = $this->get($key); | |||
| $this[$key] = $value; | |||
| return $previousValue; | |||
| } | |||
| /** | |||
| * @param K $key The key to put in the map. | |||
| * @param T $value The value to store at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function putIfAbsent(int | string $key, mixed $value): mixed | |||
| { | |||
| $currentValue = $this->get($key); | |||
| if ($currentValue === null) { | |||
| $this[$key] = $value; | |||
| } | |||
| return $currentValue; | |||
| } | |||
| /** | |||
| * @param K $key The key to remove from the map. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function remove(int | string $key): mixed | |||
| { | |||
| $previousValue = $this->get($key); | |||
| unset($this[$key]); | |||
| return $previousValue; | |||
| } | |||
| public function removeIf(int | string $key, mixed $value): bool | |||
| { | |||
| if ($this->get($key) === $value) { | |||
| unset($this[$key]); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * @param K $key The key to replace. | |||
| * @param T $value The value to set at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function replace(int | string $key, mixed $value): mixed | |||
| { | |||
| $currentValue = $this->get($key); | |||
| if ($this->containsKey($key)) { | |||
| $this[$key] = $value; | |||
| } | |||
| return $currentValue; | |||
| } | |||
| public function replaceIf(int | string $key, mixed $oldValue, mixed $newValue): bool | |||
| { | |||
| if ($this->get($key) === $oldValue) { | |||
| $this[$key] = $newValue; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * @return array<K, T> | |||
| */ | |||
| public function __serialize(): array | |||
| { | |||
| return parent::__serialize(); | |||
| } | |||
| /** | |||
| * @return array<K, T> | |||
| */ | |||
| public function toArray(): array | |||
| { | |||
| return parent::toArray(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,60 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Tool\TypeTrait; | |||
| use Ramsey\Collection\Tool\ValueToStringTrait; | |||
| /** | |||
| * This class provides a basic implementation of `TypedMapInterface`, to | |||
| * minimize the effort required to implement this interface. | |||
| * | |||
| * @template K of array-key | |||
| * @template T | |||
| * @extends AbstractMap<K, T> | |||
| * @implements TypedMapInterface<K, T> | |||
| */ | |||
| abstract class AbstractTypedMap extends AbstractMap implements TypedMapInterface | |||
| { | |||
| use TypeTrait; | |||
| use ValueToStringTrait; | |||
| /** | |||
| * @param K $offset | |||
| * @param T $value | |||
| * | |||
| * @inheritDoc | |||
| * @psalm-suppress MoreSpecificImplementedParamType | |||
| */ | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($this->checkType($this->getKeyType(), $offset) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Key must be of type ' . $this->getKeyType() . '; key is ' | |||
| . $this->toolValueToString($offset), | |||
| ); | |||
| } | |||
| if ($this->checkType($this->getValueType(), $value) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Value must be of type ' . $this->getValueType() . '; value is ' | |||
| . $this->toolValueToString($value), | |||
| ); | |||
| } | |||
| parent::offsetSet($offset, $value); | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| /** | |||
| * `AssociativeArrayMap` represents a standard associative array object. | |||
| * | |||
| * @extends AbstractMap<string, mixed> | |||
| */ | |||
| class AssociativeArrayMap extends AbstractMap | |||
| { | |||
| } | |||
| @@ -0,0 +1,142 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| use Ramsey\Collection\ArrayInterface; | |||
| /** | |||
| * An object that maps keys to values. | |||
| * | |||
| * A map cannot contain duplicate keys; each key can map to at most one value. | |||
| * | |||
| * @template K of array-key | |||
| * @template T | |||
| * @extends ArrayInterface<T> | |||
| */ | |||
| interface MapInterface extends ArrayInterface | |||
| { | |||
| /** | |||
| * Returns `true` if this map contains a mapping for the specified key. | |||
| * | |||
| * @param K $key The key to check in the map. | |||
| */ | |||
| public function containsKey(int | string $key): bool; | |||
| /** | |||
| * Returns `true` if this map maps one or more keys to the specified value. | |||
| * | |||
| * This performs a strict type check on the value. | |||
| * | |||
| * @param T $value The value to check in the map. | |||
| */ | |||
| public function containsValue(mixed $value): bool; | |||
| /** | |||
| * Return an array of the keys contained in this map. | |||
| * | |||
| * @return list<K> | |||
| */ | |||
| public function keys(): array; | |||
| /** | |||
| * Returns the value to which the specified key is mapped, `null` if this | |||
| * map contains no mapping for the key, or (optionally) `$defaultValue` if | |||
| * this map contains no mapping for the key. | |||
| * | |||
| * @param K $key The key to return from the map. | |||
| * @param T | null $defaultValue The default value to use if `$key` is not found. | |||
| * | |||
| * @return T | null the value or `null` if the key could not be found. | |||
| */ | |||
| public function get(int | string $key, mixed $defaultValue = null): mixed; | |||
| /** | |||
| * Associates the specified value with the specified key in this map. | |||
| * | |||
| * If the map previously contained a mapping for the key, the old value is | |||
| * replaced by the specified value. | |||
| * | |||
| * @param K $key The key to put or replace in the map. | |||
| * @param T $value The value to store at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function put(int | string $key, mixed $value): mixed; | |||
| /** | |||
| * Associates the specified value with the specified key in this map only if | |||
| * it is not already set. | |||
| * | |||
| * If there is already a value associated with `$key`, this returns that | |||
| * value without replacing it. | |||
| * | |||
| * @param K $key The key to put in the map. | |||
| * @param T $value The value to store at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function putIfAbsent(int | string $key, mixed $value): mixed; | |||
| /** | |||
| * Removes the mapping for a key from this map if it is present. | |||
| * | |||
| * @param K $key The key to remove from the map. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function remove(int | string $key): mixed; | |||
| /** | |||
| * Removes the entry for the specified key only if it is currently mapped to | |||
| * the specified value. | |||
| * | |||
| * This performs a strict type check on the value. | |||
| * | |||
| * @param K $key The key to remove from the map. | |||
| * @param T $value The value to match. | |||
| * | |||
| * @return bool true if the value was removed. | |||
| */ | |||
| public function removeIf(int | string $key, mixed $value): bool; | |||
| /** | |||
| * Replaces the entry for the specified key only if it is currently mapped | |||
| * to some value. | |||
| * | |||
| * @param K $key The key to replace. | |||
| * @param T $value The value to set at `$key`. | |||
| * | |||
| * @return T | null the previous value associated with key, or `null` if | |||
| * there was no mapping for `$key`. | |||
| */ | |||
| public function replace(int | string $key, mixed $value): mixed; | |||
| /** | |||
| * Replaces the entry for the specified key only if currently mapped to the | |||
| * specified value. | |||
| * | |||
| * This performs a strict type check on the value. | |||
| * | |||
| * @param K $key The key to remove from the map. | |||
| * @param T $oldValue The value to match. | |||
| * @param T $newValue The value to use as a replacement. | |||
| * | |||
| * @return bool true if the value was replaced. | |||
| */ | |||
| public function replaceIf(int | string $key, mixed $oldValue, mixed $newValue): bool; | |||
| } | |||
| @@ -0,0 +1,110 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Tool\TypeTrait; | |||
| use Ramsey\Collection\Tool\ValueToStringTrait; | |||
| use function array_combine; | |||
| use function array_key_exists; | |||
| use function is_int; | |||
| /** | |||
| * `NamedParameterMap` represents a mapping of values to a set of named keys | |||
| * that may optionally be typed | |||
| * | |||
| * @extends AbstractMap<string, mixed> | |||
| */ | |||
| class NamedParameterMap extends AbstractMap | |||
| { | |||
| use TypeTrait; | |||
| use ValueToStringTrait; | |||
| /** | |||
| * Named parameters defined for this map. | |||
| * | |||
| * @var array<string, string> | |||
| */ | |||
| private readonly array $namedParameters; | |||
| /** | |||
| * Constructs a new `NamedParameterMap`. | |||
| * | |||
| * @param array<array-key, string> $namedParameters The named parameters defined for this map. | |||
| * @param array<string, mixed> $data An initial set of data to set on this map. | |||
| */ | |||
| public function __construct(array $namedParameters, array $data = []) | |||
| { | |||
| $this->namedParameters = $this->filterNamedParameters($namedParameters); | |||
| parent::__construct($data); | |||
| } | |||
| /** | |||
| * Returns named parameters set for this `NamedParameterMap`. | |||
| * | |||
| * @return array<string, string> | |||
| */ | |||
| public function getNamedParameters(): array | |||
| { | |||
| return $this->namedParameters; | |||
| } | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if (!array_key_exists($offset, $this->namedParameters)) { | |||
| throw new InvalidArgumentException( | |||
| 'Attempting to set value for unconfigured parameter \'' | |||
| . $this->toolValueToString($offset) . '\'', | |||
| ); | |||
| } | |||
| if ($this->checkType($this->namedParameters[$offset], $value) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Value for \'' . $offset . '\' must be of type ' | |||
| . $this->namedParameters[$offset] . '; value is ' | |||
| . $this->toolValueToString($value), | |||
| ); | |||
| } | |||
| $this->data[$offset] = $value; | |||
| } | |||
| /** | |||
| * Given an array of named parameters, constructs a proper mapping of | |||
| * named parameters to types. | |||
| * | |||
| * @param array<array-key, string> $namedParameters The named parameters to filter. | |||
| * | |||
| * @return array<string, string> | |||
| */ | |||
| protected function filterNamedParameters(array $namedParameters): array | |||
| { | |||
| $names = []; | |||
| $types = []; | |||
| foreach ($namedParameters as $key => $value) { | |||
| if (is_int($key)) { | |||
| $names[] = $value; | |||
| $types[] = 'mixed'; | |||
| } else { | |||
| $names[] = $key; | |||
| $types[] = $value; | |||
| } | |||
| } | |||
| return array_combine($names, $types) ?: []; | |||
| } | |||
| } | |||
| @@ -0,0 +1,112 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| /** | |||
| * A `TypedMap` represents a map of elements where key and value are typed. | |||
| * | |||
| * Each element is identified by a key with defined type and a value of defined | |||
| * type. The keys of the map must be unique. The values on the map can be | |||
| * repeated but each with its own different key. | |||
| * | |||
| * The most common case is to use a string type key, but it's not limited to | |||
| * this type of keys. | |||
| * | |||
| * This is a direct implementation of `TypedMapInterface`, provided for the sake | |||
| * of convenience. | |||
| * | |||
| * Example usage: | |||
| * | |||
| * ```php | |||
| * $map = new TypedMap('string', Foo::class); | |||
| * $map['x'] = new Foo(); | |||
| * foreach ($map as $key => $value) { | |||
| * // do something with $key, it will be a Foo::class | |||
| * } | |||
| * | |||
| * // this will throw an exception since key must be string | |||
| * $map[10] = new Foo(); | |||
| * | |||
| * // this will throw an exception since value must be a Foo | |||
| * $map['bar'] = 'bar'; | |||
| * | |||
| * // initialize map with contents | |||
| * $map = new TypedMap('string', Foo::class, [ | |||
| * new Foo(), new Foo(), new Foo() | |||
| * ]); | |||
| * ``` | |||
| * | |||
| * It is preferable to subclass `AbstractTypedMap` to create your own typed map | |||
| * implementation: | |||
| * | |||
| * ```php | |||
| * class FooTypedMap extends AbstractTypedMap | |||
| * { | |||
| * public function getKeyType() | |||
| * { | |||
| * return 'int'; | |||
| * } | |||
| * | |||
| * public function getValueType() | |||
| * { | |||
| * return Foo::class; | |||
| * } | |||
| * } | |||
| * ``` | |||
| * | |||
| * … but you also may use the `TypedMap` class: | |||
| * | |||
| * ```php | |||
| * class FooTypedMap extends TypedMap | |||
| * { | |||
| * public function __constructor(array $data = []) | |||
| * { | |||
| * parent::__construct('int', Foo::class, $data); | |||
| * } | |||
| * } | |||
| * ``` | |||
| * | |||
| * @template K of array-key | |||
| * @template T | |||
| * @extends AbstractTypedMap<K, T> | |||
| */ | |||
| class TypedMap extends AbstractTypedMap | |||
| { | |||
| /** | |||
| * Constructs a map object of the specified key and value types, | |||
| * optionally with the specified data. | |||
| * | |||
| * @param string $keyType The data type of the map's keys. | |||
| * @param string $valueType The data type of the map's values. | |||
| * @param array<K, T> $data The initial data to set for this map. | |||
| */ | |||
| public function __construct( | |||
| private readonly string $keyType, | |||
| private readonly string $valueType, | |||
| array $data = [], | |||
| ) { | |||
| parent::__construct($data); | |||
| } | |||
| public function getKeyType(): string | |||
| { | |||
| return $this->keyType; | |||
| } | |||
| public function getValueType(): string | |||
| { | |||
| return $this->valueType; | |||
| } | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Map; | |||
| /** | |||
| * A `TypedMapInterface` represents a map of elements where key and value are | |||
| * typed. | |||
| * | |||
| * @template K of array-key | |||
| * @template T | |||
| * @extends MapInterface<K, T> | |||
| */ | |||
| interface TypedMapInterface extends MapInterface | |||
| { | |||
| /** | |||
| * Return the type used on the key. | |||
| */ | |||
| public function getKeyType(): string; | |||
| /** | |||
| * Return the type forced on the values. | |||
| */ | |||
| public function getValueType(): string; | |||
| } | |||
| @@ -0,0 +1,148 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Ramsey\Collection\Exception\InvalidArgumentException; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use Ramsey\Collection\Tool\TypeTrait; | |||
| use Ramsey\Collection\Tool\ValueToStringTrait; | |||
| use function array_key_first; | |||
| /** | |||
| * This class provides a basic implementation of `QueueInterface`, to minimize | |||
| * the effort required to implement this interface. | |||
| * | |||
| * @template T | |||
| * @extends AbstractArray<T> | |||
| * @implements QueueInterface<T> | |||
| */ | |||
| class Queue extends AbstractArray implements QueueInterface | |||
| { | |||
| use TypeTrait; | |||
| use ValueToStringTrait; | |||
| /** | |||
| * Constructs a queue object of the specified type, optionally with the | |||
| * specified data. | |||
| * | |||
| * @param string $queueType The type or class name associated with this queue. | |||
| * @param array<array-key, T> $data The initial items to store in the queue. | |||
| */ | |||
| public function __construct(private readonly string $queueType, array $data = []) | |||
| { | |||
| parent::__construct($data); | |||
| } | |||
| /** | |||
| * {@inheritDoc} | |||
| * | |||
| * Since arbitrary offsets may not be manipulated in a queue, this method | |||
| * serves only to fulfill the `ArrayAccess` interface requirements. It is | |||
| * invoked by other operations when adding values to the queue. | |||
| * | |||
| * @throws InvalidArgumentException if $value is of the wrong type. | |||
| */ | |||
| public function offsetSet(mixed $offset, mixed $value): void | |||
| { | |||
| if ($this->checkType($this->getType(), $value) === false) { | |||
| throw new InvalidArgumentException( | |||
| 'Value must be of type ' . $this->getType() . '; value is ' | |||
| . $this->toolValueToString($value), | |||
| ); | |||
| } | |||
| $this->data[] = $value; | |||
| } | |||
| /** | |||
| * @throws InvalidArgumentException if $value is of the wrong type. | |||
| */ | |||
| public function add(mixed $element): bool | |||
| { | |||
| $this[] = $element; | |||
| return true; | |||
| } | |||
| /** | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function element(): mixed | |||
| { | |||
| return $this->peek() ?? throw new NoSuchElementException( | |||
| 'Can\'t return element from Queue. Queue is empty.', | |||
| ); | |||
| } | |||
| public function offer(mixed $element): bool | |||
| { | |||
| try { | |||
| return $this->add($element); | |||
| } catch (InvalidArgumentException) { | |||
| return false; | |||
| } | |||
| } | |||
| /** | |||
| * @return T | null | |||
| */ | |||
| public function peek(): mixed | |||
| { | |||
| $index = array_key_first($this->data); | |||
| if ($index === null) { | |||
| return null; | |||
| } | |||
| return $this[$index]; | |||
| } | |||
| /** | |||
| * @return T | null | |||
| */ | |||
| public function poll(): mixed | |||
| { | |||
| $index = array_key_first($this->data); | |||
| if ($index === null) { | |||
| return null; | |||
| } | |||
| $head = $this[$index]; | |||
| unset($this[$index]); | |||
| return $head; | |||
| } | |||
| /** | |||
| * @return T | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function remove(): mixed | |||
| { | |||
| return $this->poll() ?? throw new NoSuchElementException( | |||
| 'Can\'t return element from Queue. Queue is empty.', | |||
| ); | |||
| } | |||
| public function getType(): string | |||
| { | |||
| return $this->queueType; | |||
| } | |||
| } | |||
| @@ -0,0 +1,202 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| use Ramsey\Collection\Exception\NoSuchElementException; | |||
| use RuntimeException; | |||
| /** | |||
| * A queue is a collection in which the entities in the collection are kept in | |||
| * order. | |||
| * | |||
| * The principal operations on the queue are the addition of entities to the end | |||
| * (tail), also known as *enqueue*, and removal of entities from the front | |||
| * (head), also known as *dequeue*. This makes the queue a first-in-first-out | |||
| * (FIFO) data structure. | |||
| * | |||
| * Besides basic array operations, queues provide additional insertion, | |||
| * extraction, and inspection operations. Each of these methods exists in two | |||
| * forms: one throws an exception if the operation fails, the other returns a | |||
| * special value (either `null` or `false`, depending on the operation). The | |||
| * latter form of the insert operation is designed specifically for use with | |||
| * capacity-restricted `QueueInterface` implementations; in most | |||
| * implementations, insert operations cannot fail. | |||
| * | |||
| * <table> | |||
| * <caption>Summary of QueueInterface methods</caption> | |||
| * <thead> | |||
| * <tr> | |||
| * <td></td> | |||
| * <td><em>Throws exception</em></td> | |||
| * <td><em>Returns special value</em></td> | |||
| * </tr> | |||
| * </thead> | |||
| * <tbody> | |||
| * <tr> | |||
| * <th>Insert</th> | |||
| * <td><code>add()</code></td> | |||
| * <td><code>offer()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <th>Remove</th> | |||
| * <td><code>remove()</code></td> | |||
| * <td><code>poll()</code></td> | |||
| * </tr> | |||
| * <tr> | |||
| * <th>Examine</th> | |||
| * <td><code>element()</code></td> | |||
| * <td><code>peek()</code></td> | |||
| * </tr> | |||
| * </tbody> | |||
| * </table> | |||
| * | |||
| * Queues typically, but do not necessarily, order elements in a FIFO | |||
| * (first-in-first-out) manner. Among the exceptions are priority queues, which | |||
| * order elements according to a supplied comparator, or the elements' natural | |||
| * ordering, and LIFO queues (or stacks) which order the elements LIFO | |||
| * (last-in-first-out). Whatever the ordering used, the head of the queue is | |||
| * that element which would be removed by a call to remove() or poll(). In a | |||
| * FIFO queue, all new elements are inserted at the tail of the queue. Other | |||
| * kinds of queues may use different placement rules. Every `QueueInterface` | |||
| * implementation must specify its ordering properties. | |||
| * | |||
| * The `offer()` method inserts an element if possible, otherwise returning | |||
| * `false`. This differs from the `add()` method, which can fail to add an | |||
| * element only by throwing an unchecked exception. The `offer()` method is | |||
| * designed for use when failure is a normal, rather than exceptional | |||
| * occurrence, for example, in fixed-capacity (or "bounded") queues. | |||
| * | |||
| * The `remove()` and `poll()` methods remove and return the head of the queue. | |||
| * Exactly which element is removed from the queue is a function of the queue's | |||
| * ordering policy, which differs from implementation to implementation. The | |||
| * `remove()` and `poll()` methods differ only in their behavior when the queue | |||
| * is empty: the `remove()` method throws an exception, while the `poll()` | |||
| * method returns `null`. | |||
| * | |||
| * The `element()` and `peek()` methods return, but do not remove, the head of | |||
| * the queue. | |||
| * | |||
| * `QueueInterface` implementations generally do not allow insertion of `null` | |||
| * elements, although some implementations do not prohibit insertion of `null`. | |||
| * Even in the implementations that permit it, `null` should not be inserted | |||
| * into a queue, as `null` is also used as a special return value by the | |||
| * `poll()` method to indicate that the queue contains no elements. | |||
| * | |||
| * @template T | |||
| * @extends ArrayInterface<T> | |||
| */ | |||
| interface QueueInterface extends ArrayInterface | |||
| { | |||
| /** | |||
| * Ensures that this queue contains the specified element (optional | |||
| * operation). | |||
| * | |||
| * Returns `true` if this queue changed as a result of the call. (Returns | |||
| * `false` if this queue does not permit duplicates and already contains the | |||
| * specified element.) | |||
| * | |||
| * Queues that support this operation may place limitations on what elements | |||
| * may be added to this queue. In particular, some queues will refuse to add | |||
| * `null` elements, and others will impose restrictions on the type of | |||
| * elements that may be added. Queue classes should clearly specify in their | |||
| * documentation any restrictions on what elements may be added. | |||
| * | |||
| * If a queue refuses to add a particular element for any reason other than | |||
| * that it already contains the element, it must throw an exception (rather | |||
| * than returning `false`). This preserves the invariant that a queue always | |||
| * contains the specified element after this call returns. | |||
| * | |||
| * @see self::offer() | |||
| * | |||
| * @param T $element The element to add to this queue. | |||
| * | |||
| * @return bool `true` if this queue changed as a result of the call. | |||
| * | |||
| * @throws RuntimeException if a queue refuses to add a particular element | |||
| * for any reason other than that it already contains the element. | |||
| * Implementations should use a more-specific exception that extends | |||
| * `\RuntimeException`. | |||
| */ | |||
| public function add(mixed $element): bool; | |||
| /** | |||
| * Retrieves, but does not remove, the head of this queue. | |||
| * | |||
| * This method differs from `peek()` only in that it throws an exception if | |||
| * this queue is empty. | |||
| * | |||
| * @see self::peek() | |||
| * | |||
| * @return T the head of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function element(): mixed; | |||
| /** | |||
| * Inserts the specified element into this queue if it is possible to do so | |||
| * immediately without violating capacity restrictions. | |||
| * | |||
| * When using a capacity-restricted queue, this method is generally | |||
| * preferable to `add()`, which can fail to insert an element only by | |||
| * throwing an exception. | |||
| * | |||
| * @see self::add() | |||
| * | |||
| * @param T $element The element to add to this queue. | |||
| * | |||
| * @return bool `true` if the element was added to this queue, else `false`. | |||
| */ | |||
| public function offer(mixed $element): bool; | |||
| /** | |||
| * Retrieves, but does not remove, the head of this queue, or returns `null` | |||
| * if this queue is empty. | |||
| * | |||
| * @see self::element() | |||
| * | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function peek(): mixed; | |||
| /** | |||
| * Retrieves and removes the head of this queue, or returns `null` | |||
| * if this queue is empty. | |||
| * | |||
| * @see self::remove() | |||
| * | |||
| * @return T | null the head of this queue, or `null` if this queue is empty. | |||
| */ | |||
| public function poll(): mixed; | |||
| /** | |||
| * Retrieves and removes the head of this queue. | |||
| * | |||
| * This method differs from `poll()` only in that it throws an exception if | |||
| * this queue is empty. | |||
| * | |||
| * @see self::poll() | |||
| * | |||
| * @return T the head of this queue. | |||
| * | |||
| * @throws NoSuchElementException if this queue is empty. | |||
| */ | |||
| public function remove(): mixed; | |||
| /** | |||
| * Returns the type associated with this queue. | |||
| */ | |||
| public function getType(): string; | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| /** | |||
| * A set is a collection that contains no duplicate elements. | |||
| * | |||
| * Great care must be exercised if mutable objects are used as set elements. | |||
| * The behavior of a set is not specified if the value of an object is changed | |||
| * in a manner that affects equals comparisons while the object is an element in | |||
| * the set. | |||
| * | |||
| * Example usage: | |||
| * | |||
| * ``` php | |||
| * $foo = new \My\Foo(); | |||
| * $set = new Set(\My\Foo::class); | |||
| * | |||
| * $set->add($foo); // returns TRUE, the element doesn't exist | |||
| * $set->add($foo); // returns FALSE, the element already exists | |||
| * | |||
| * $bar = new \My\Foo(); | |||
| * $set->add($bar); // returns TRUE, $bar !== $foo | |||
| * ``` | |||
| * | |||
| * @template T | |||
| * @extends AbstractSet<T> | |||
| */ | |||
| class Set extends AbstractSet | |||
| { | |||
| /** | |||
| * Constructs a set object of the specified type, optionally with the | |||
| * specified data. | |||
| * | |||
| * @param string $setType The type or class name associated with this set. | |||
| * @param array<array-key, T> $data The initial items to store in the set. | |||
| */ | |||
| public function __construct(private readonly string $setType, array $data = []) | |||
| { | |||
| parent::__construct($data); | |||
| } | |||
| public function getType(): string | |||
| { | |||
| return $this->setType; | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection; | |||
| /** | |||
| * Collection sorting | |||
| */ | |||
| enum Sort: string | |||
| { | |||
| /** | |||
| * Sort items in a collection in ascending order. | |||
| */ | |||
| case Ascending = 'asc'; | |||
| /** | |||
| * Sort items in a collection in descending order. | |||
| */ | |||
| case Descending = 'desc'; | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Tool; | |||
| use function is_array; | |||
| use function is_bool; | |||
| use function is_callable; | |||
| use function is_float; | |||
| use function is_int; | |||
| use function is_numeric; | |||
| use function is_object; | |||
| use function is_resource; | |||
| use function is_scalar; | |||
| use function is_string; | |||
| /** | |||
| * Provides functionality to check values for specific types. | |||
| */ | |||
| trait TypeTrait | |||
| { | |||
| /** | |||
| * Returns `true` if value is of the specified type. | |||
| * | |||
| * @param string $type The type to check the value against. | |||
| * @param mixed $value The value to check. | |||
| */ | |||
| protected function checkType(string $type, mixed $value): bool | |||
| { | |||
| return match ($type) { | |||
| 'array' => is_array($value), | |||
| 'bool', 'boolean' => is_bool($value), | |||
| 'callable' => is_callable($value), | |||
| 'float', 'double' => is_float($value), | |||
| 'int', 'integer' => is_int($value), | |||
| 'null' => $value === null, | |||
| 'numeric' => is_numeric($value), | |||
| 'object' => is_object($value), | |||
| 'resource' => is_resource($value), | |||
| 'scalar' => is_scalar($value), | |||
| 'string' => is_string($value), | |||
| 'mixed' => true, | |||
| default => $value instanceof $type, | |||
| }; | |||
| } | |||
| } | |||
| @@ -0,0 +1,81 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Tool; | |||
| use Ramsey\Collection\Exception\InvalidPropertyOrMethod; | |||
| use Ramsey\Collection\Exception\UnsupportedOperationException; | |||
| use function is_array; | |||
| use function is_object; | |||
| use function method_exists; | |||
| use function property_exists; | |||
| use function sprintf; | |||
| /** | |||
| * Provides functionality to extract the value of a property or method from an object. | |||
| */ | |||
| trait ValueExtractorTrait | |||
| { | |||
| /** | |||
| * Extracts the value of the given property, method, or array key from the | |||
| * element. | |||
| * | |||
| * If `$propertyOrMethod` is `null`, we return the element as-is. | |||
| * | |||
| * @param mixed $element The element to extract the value from. | |||
| * @param string | null $propertyOrMethod The property or method for which the | |||
| * value should be extracted. | |||
| * | |||
| * @return mixed the value extracted from the specified property, method, | |||
| * or array key, or the element itself. | |||
| * | |||
| * @throws InvalidPropertyOrMethod | |||
| * @throws UnsupportedOperationException | |||
| */ | |||
| protected function extractValue(mixed $element, ?string $propertyOrMethod): mixed | |||
| { | |||
| if ($propertyOrMethod === null) { | |||
| return $element; | |||
| } | |||
| if (!is_object($element) && !is_array($element)) { | |||
| throw new UnsupportedOperationException(sprintf( | |||
| 'The collection type "%s" does not support the $propertyOrMethod parameter', | |||
| $this->getType(), | |||
| )); | |||
| } | |||
| if (is_array($element)) { | |||
| return $element[$propertyOrMethod] ?? throw new InvalidPropertyOrMethod(sprintf( | |||
| 'Key or index "%s" not found in collection elements', | |||
| $propertyOrMethod, | |||
| )); | |||
| } | |||
| if (property_exists($element, $propertyOrMethod)) { | |||
| return $element->$propertyOrMethod; | |||
| } | |||
| if (method_exists($element, $propertyOrMethod)) { | |||
| return $element->{$propertyOrMethod}(); | |||
| } | |||
| throw new InvalidPropertyOrMethod(sprintf( | |||
| 'Method or property "%s" not defined in %s', | |||
| $propertyOrMethod, | |||
| $element::class, | |||
| )); | |||
| } | |||
| } | |||
| @@ -0,0 +1,91 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/collection library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Collection\Tool; | |||
| use DateTimeInterface; | |||
| use function assert; | |||
| use function get_resource_type; | |||
| use function is_array; | |||
| use function is_bool; | |||
| use function is_callable; | |||
| use function is_object; | |||
| use function is_resource; | |||
| use function is_scalar; | |||
| /** | |||
| * Provides functionality to express a value as string | |||
| */ | |||
| trait ValueToStringTrait | |||
| { | |||
| /** | |||
| * Returns a string representation of the value. | |||
| * | |||
| * - null value: `'NULL'` | |||
| * - boolean: `'TRUE'`, `'FALSE'` | |||
| * - array: `'Array'` | |||
| * - scalar: converted-value | |||
| * - resource: `'(type resource #number)'` | |||
| * - object with `__toString()`: result of `__toString()` | |||
| * - object DateTime: ISO 8601 date | |||
| * - object: `'(className Object)'` | |||
| * - anonymous function: same as object | |||
| * | |||
| * @param mixed $value the value to return as a string. | |||
| */ | |||
| protected function toolValueToString(mixed $value): string | |||
| { | |||
| // null | |||
| if ($value === null) { | |||
| return 'NULL'; | |||
| } | |||
| // boolean constants | |||
| if (is_bool($value)) { | |||
| return $value ? 'TRUE' : 'FALSE'; | |||
| } | |||
| // array | |||
| if (is_array($value)) { | |||
| return 'Array'; | |||
| } | |||
| // scalar types (integer, float, string) | |||
| if (is_scalar($value)) { | |||
| return (string) $value; | |||
| } | |||
| // resource | |||
| if (is_resource($value)) { | |||
| return '(' . get_resource_type($value) . ' resource #' . (int) $value . ')'; | |||
| } | |||
| // From here, $value should be an object. | |||
| assert(is_object($value)); | |||
| // __toString() is implemented | |||
| if (is_callable([$value, '__toString'])) { | |||
| return (string) $value->__toString(); | |||
| } | |||
| // object of type \DateTime | |||
| if ($value instanceof DateTimeInterface) { | |||
| return $value->format('c'); | |||
| } | |||
| // unknown type | |||
| return '(' . $value::class . ' Object)'; | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| Copyright (c) 2012-2023 Ben Ramsey <ben@benramsey.com> | |||
| 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. | |||
| @@ -0,0 +1,83 @@ | |||
| <h1 align="center">ramsey/uuid</h1> | |||
| <p align="center"> | |||
| <strong>A PHP library for generating and working with UUIDs.</strong> | |||
| </p> | |||
| <p align="center"> | |||
| <a href="https://github.com/ramsey/uuid"><img src="http://img.shields.io/badge/source-ramsey/uuid-blue.svg?style=flat-square" alt="Source Code"></a> | |||
| <a href="https://packagist.org/packages/ramsey/uuid"><img src="https://img.shields.io/packagist/v/ramsey/uuid.svg?style=flat-square&label=release" alt="Download Package"></a> | |||
| <a href="https://php.net"><img src="https://img.shields.io/packagist/php-v/ramsey/uuid.svg?style=flat-square&colorB=%238892BF" alt="PHP Programming Language"></a> | |||
| <a href="https://github.com/ramsey/uuid/blob/4.x/LICENSE"><img src="https://img.shields.io/packagist/l/ramsey/uuid.svg?style=flat-square&colorB=darkcyan" alt="Read License"></a> | |||
| <a href="https://github.com/ramsey/uuid/actions/workflows/continuous-integration.yml"><img src="https://img.shields.io/github/actions/workflow/status/ramsey/uuid/continuous-integration.yml?branch=4.x&logo=github&style=flat-square" alt="Build Status"></a> | |||
| <a href="https://app.codecov.io/gh/ramsey/uuid/branch/4.x"><img src="https://img.shields.io/codecov/c/github/ramsey/uuid/4.x?label=codecov&logo=codecov&style=flat-square" alt="Codecov Code Coverage"></a> | |||
| <a href="https://shepherd.dev/github/ramsey/uuid"><img src="https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fshepherd.dev%2Fgithub%2Framsey%2Fuuid%2Fcoverage" alt="Psalm Type Coverage"></a> | |||
| </p> | |||
| ramsey/uuid is a PHP library for generating and working with universally unique | |||
| identifiers (UUIDs). | |||
| This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). | |||
| By participating in this project and its community, you are expected to | |||
| uphold this code. | |||
| Much inspiration for this library came from the [Java][javauuid] and | |||
| [Python][pyuuid] UUID libraries. | |||
| ## Installation | |||
| The preferred method of installation is via [Composer][]. Run the following | |||
| command to install the package and add it as a requirement to your project's | |||
| `composer.json`: | |||
| ```bash | |||
| composer require ramsey/uuid | |||
| ``` | |||
| ## Upgrading to Version 4 | |||
| See the documentation for a thorough upgrade guide: | |||
| * [Upgrading ramsey/uuid Version 3 to 4](https://uuid.ramsey.dev/en/stable/upgrading/3-to-4.html) | |||
| ## Documentation | |||
| Please see <https://uuid.ramsey.dev> for documentation, tips, examples, and | |||
| frequently asked questions. | |||
| ## Contributing | |||
| Contributions are welcome! To contribute, please familiarize yourself with | |||
| [CONTRIBUTING.md](CONTRIBUTING.md). | |||
| ## Coordinated Disclosure | |||
| Keeping user information safe and secure is a top priority, and we welcome the | |||
| contribution of external security researchers. If you believe you've found a | |||
| security issue in software that is maintained in this repository, please read | |||
| [SECURITY.md][] for instructions on submitting a vulnerability report. | |||
| ## ramsey/uuid for Enterprise | |||
| Available as part of the Tidelift Subscription. | |||
| The maintainers of ramsey/uuid and thousands of other packages are working with | |||
| Tidelift to deliver commercial support and maintenance for the open source | |||
| packages you use to build your applications. Save time, reduce risk, and improve | |||
| code health, while paying the maintainers of the exact packages you use. | |||
| [Learn more.](https://tidelift.com/subscription/pkg/packagist-ramsey-uuid?utm_source=undefined&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) | |||
| ## Copyright and License | |||
| The ramsey/uuid library is copyright © [Ben Ramsey](https://benramsey.com/) and | |||
| licensed for use under the MIT License (MIT). Please see [LICENSE][] for more | |||
| information. | |||
| [rfc4122]: http://tools.ietf.org/html/rfc4122 | |||
| [conduct]: https://github.com/ramsey/uuid/blob/4.x/CODE_OF_CONDUCT.md | |||
| [javauuid]: http://docs.oracle.com/javase/6/docs/api/java/util/UUID.html | |||
| [pyuuid]: http://docs.python.org/3/library/uuid.html | |||
| [composer]: http://getcomposer.org/ | |||
| [contributing.md]: https://github.com/ramsey/uuid/blob/4.x/CONTRIBUTING.md | |||
| [security.md]: https://github.com/ramsey/uuid/blob/4.x/SECURITY.md | |||
| [license]: https://github.com/ramsey/uuid/blob/4.x/LICENSE | |||
| @@ -0,0 +1,108 @@ | |||
| { | |||
| "name": "ramsey/uuid", | |||
| "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", | |||
| "license": "MIT", | |||
| "type": "library", | |||
| "keywords": [ | |||
| "uuid", | |||
| "identifier", | |||
| "guid" | |||
| ], | |||
| "require": { | |||
| "php": "^8.0", | |||
| "ext-json": "*", | |||
| "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", | |||
| "ramsey/collection": "^1.2 || ^2.0" | |||
| }, | |||
| "require-dev": { | |||
| "captainhook/captainhook": "^5.10", | |||
| "captainhook/plugin-composer": "^5.3", | |||
| "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", | |||
| "doctrine/annotations": "^1.8", | |||
| "ergebnis/composer-normalize": "^2.15", | |||
| "mockery/mockery": "^1.3", | |||
| "paragonie/random-lib": "^2", | |||
| "php-mock/php-mock": "^2.2", | |||
| "php-mock/php-mock-mockery": "^1.3", | |||
| "php-parallel-lint/php-parallel-lint": "^1.1", | |||
| "phpbench/phpbench": "^1.0", | |||
| "phpstan/extension-installer": "^1.1", | |||
| "phpstan/phpstan": "^1.8", | |||
| "phpstan/phpstan-mockery": "^1.1", | |||
| "phpstan/phpstan-phpunit": "^1.1", | |||
| "phpunit/phpunit": "^8.5 || ^9", | |||
| "ramsey/composer-repl": "^1.4", | |||
| "slevomat/coding-standard": "^8.4", | |||
| "squizlabs/php_codesniffer": "^3.5", | |||
| "vimeo/psalm": "^4.9" | |||
| }, | |||
| "replace": { | |||
| "rhumsaa/uuid": "self.version" | |||
| }, | |||
| "suggest": { | |||
| "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", | |||
| "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", | |||
| "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", | |||
| "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", | |||
| "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." | |||
| }, | |||
| "minimum-stability": "dev", | |||
| "prefer-stable": true, | |||
| "autoload": { | |||
| "psr-4": { | |||
| "Ramsey\\Uuid\\": "src/" | |||
| }, | |||
| "files": [ | |||
| "src/functions.php" | |||
| ] | |||
| }, | |||
| "autoload-dev": { | |||
| "psr-4": { | |||
| "Ramsey\\Uuid\\Benchmark\\": "tests/benchmark/", | |||
| "Ramsey\\Uuid\\StaticAnalysis\\": "tests/static-analysis/", | |||
| "Ramsey\\Uuid\\Test\\": "tests/" | |||
| } | |||
| }, | |||
| "config": { | |||
| "allow-plugins": { | |||
| "captainhook/plugin-composer": true, | |||
| "ergebnis/composer-normalize": true, | |||
| "phpstan/extension-installer": true, | |||
| "dealerdirect/phpcodesniffer-composer-installer": true, | |||
| "ramsey/composer-repl": true | |||
| }, | |||
| "sort-packages": true | |||
| }, | |||
| "extra": { | |||
| "captainhook": { | |||
| "force-install": true | |||
| } | |||
| }, | |||
| "scripts": { | |||
| "analyze": [ | |||
| "@phpstan", | |||
| "@psalm" | |||
| ], | |||
| "build:clean": "git clean -fX build/", | |||
| "lint": "parallel-lint src tests", | |||
| "lint:paths": "parallel-lint", | |||
| "phpbench": "phpbench run", | |||
| "phpcbf": "phpcbf -vpw --cache=build/cache/phpcs.cache", | |||
| "phpcs": "phpcs --cache=build/cache/phpcs.cache", | |||
| "phpstan": [ | |||
| "phpstan analyse --no-progress --memory-limit=1G", | |||
| "phpstan analyse -c phpstan-tests.neon --no-progress --memory-limit=1G" | |||
| ], | |||
| "phpunit": "phpunit --verbose --colors=always", | |||
| "phpunit-coverage": "phpunit --verbose --colors=always --coverage-html build/coverage", | |||
| "psalm": "psalm --show-info=false --config=psalm.xml", | |||
| "test": [ | |||
| "@lint", | |||
| "@phpbench", | |||
| "@phpcs", | |||
| "@phpstan", | |||
| "@psalm", | |||
| "@phpunit" | |||
| ] | |||
| } | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid; | |||
| /** | |||
| * Provides binary math utilities | |||
| */ | |||
| class BinaryUtils | |||
| { | |||
| /** | |||
| * Applies the RFC 4122 variant field to the 16-bit clock sequence | |||
| * | |||
| * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant | |||
| * | |||
| * @param int $clockSeq The 16-bit clock sequence value before the RFC 4122 | |||
| * variant is applied | |||
| * | |||
| * @return int The 16-bit clock sequence multiplexed with the UUID variant | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function applyVariant(int $clockSeq): int | |||
| { | |||
| $clockSeq = $clockSeq & 0x3fff; | |||
| $clockSeq |= 0x8000; | |||
| return $clockSeq; | |||
| } | |||
| /** | |||
| * Applies the RFC 4122 version number to the 16-bit `time_hi_and_version` field | |||
| * | |||
| * @link http://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version | |||
| * | |||
| * @param int $timeHi The value of the 16-bit `time_hi_and_version` field | |||
| * before the RFC 4122 version is applied | |||
| * @param int $version The RFC 4122 version to apply to the `time_hi` field | |||
| * | |||
| * @return int The 16-bit time_hi field of the timestamp multiplexed with | |||
| * the UUID version number | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public static function applyVersion(int $timeHi, int $version): int | |||
| { | |||
| $timeHi = $timeHi & 0x0fff; | |||
| $timeHi |= $version << 12; | |||
| return $timeHi; | |||
| } | |||
| } | |||
| @@ -0,0 +1,85 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Builder; | |||
| use Ramsey\Collection\AbstractCollection; | |||
| use Ramsey\Uuid\Converter\Number\GenericNumberConverter; | |||
| use Ramsey\Uuid\Converter\Time\GenericTimeConverter; | |||
| use Ramsey\Uuid\Converter\Time\PhpTimeConverter; | |||
| use Ramsey\Uuid\Guid\GuidBuilder; | |||
| use Ramsey\Uuid\Math\BrickMathCalculator; | |||
| use Ramsey\Uuid\Nonstandard\UuidBuilder as NonstandardUuidBuilder; | |||
| use Ramsey\Uuid\Rfc4122\UuidBuilder as Rfc4122UuidBuilder; | |||
| use Traversable; | |||
| /** | |||
| * A collection of UuidBuilderInterface objects | |||
| * | |||
| * @deprecated this class has been deprecated, and will be removed in 5.0.0. The use-case for this class comes from | |||
| * a pre-`phpstan/phpstan` and pre-`vimeo/psalm` ecosystem, in which type safety had to be mostly enforced | |||
| * at runtime: that is no longer necessary, now that you can safely verify your code to be correct, and use | |||
| * more generic types like `iterable<T>` instead. | |||
| * | |||
| * @extends AbstractCollection<UuidBuilderInterface> | |||
| */ | |||
| class BuilderCollection extends AbstractCollection | |||
| { | |||
| public function getType(): string | |||
| { | |||
| return UuidBuilderInterface::class; | |||
| } | |||
| /** | |||
| * @psalm-mutation-free | |||
| * @psalm-suppress ImpureMethodCall | |||
| * @psalm-suppress InvalidTemplateParam | |||
| */ | |||
| public function getIterator(): Traversable | |||
| { | |||
| return parent::getIterator(); | |||
| } | |||
| /** | |||
| * Re-constructs the object from its serialized form | |||
| * | |||
| * @param string $serialized The serialized PHP string to unserialize into | |||
| * a UuidInterface instance | |||
| * | |||
| * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint | |||
| * @psalm-suppress RedundantConditionGivenDocblockType | |||
| */ | |||
| public function unserialize($serialized): void | |||
| { | |||
| /** @var array<array-key, UuidBuilderInterface> $data */ | |||
| $data = unserialize($serialized, [ | |||
| 'allowed_classes' => [ | |||
| BrickMathCalculator::class, | |||
| GenericNumberConverter::class, | |||
| GenericTimeConverter::class, | |||
| GuidBuilder::class, | |||
| NonstandardUuidBuilder::class, | |||
| PhpTimeConverter::class, | |||
| Rfc4122UuidBuilder::class, | |||
| ], | |||
| ]); | |||
| $this->data = array_filter( | |||
| $data, | |||
| function ($unserialized): bool { | |||
| return $unserialized instanceof UuidBuilderInterface; | |||
| } | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Builder; | |||
| use Ramsey\Uuid\Rfc4122\UuidBuilder as Rfc4122UuidBuilder; | |||
| /** | |||
| * @deprecated Transition to {@see Rfc4122UuidBuilder}. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class DefaultUuidBuilder extends Rfc4122UuidBuilder | |||
| { | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Builder; | |||
| use Ramsey\Uuid\Codec\CodecInterface; | |||
| use Ramsey\Uuid\Converter\NumberConverterInterface; | |||
| use Ramsey\Uuid\Converter\Time\DegradedTimeConverter; | |||
| use Ramsey\Uuid\Converter\TimeConverterInterface; | |||
| use Ramsey\Uuid\DegradedUuid; | |||
| use Ramsey\Uuid\Rfc4122\Fields as Rfc4122Fields; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| /** | |||
| * @deprecated DegradedUuid instances are no longer necessary to support 32-bit | |||
| * systems. Transition to {@see DefaultUuidBuilder}. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class DegradedUuidBuilder implements UuidBuilderInterface | |||
| { | |||
| private TimeConverterInterface $timeConverter; | |||
| /** | |||
| * @param NumberConverterInterface $numberConverter The number converter to | |||
| * use when constructing the DegradedUuid | |||
| * @param TimeConverterInterface|null $timeConverter The time converter to use | |||
| * for converting timestamps extracted from a UUID to Unix timestamps | |||
| */ | |||
| public function __construct( | |||
| private NumberConverterInterface $numberConverter, | |||
| ?TimeConverterInterface $timeConverter = null | |||
| ) { | |||
| $this->timeConverter = $timeConverter ?: new DegradedTimeConverter(); | |||
| } | |||
| /** | |||
| * Builds and returns a DegradedUuid | |||
| * | |||
| * @param CodecInterface $codec The codec to use for building this DegradedUuid instance | |||
| * @param string $bytes The byte string from which to construct a UUID | |||
| * | |||
| * @return DegradedUuid The DegradedUuidBuild returns an instance of Ramsey\Uuid\DegradedUuid | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public function build(CodecInterface $codec, string $bytes): UuidInterface | |||
| { | |||
| return new DegradedUuid( | |||
| new Rfc4122Fields($bytes), | |||
| $this->numberConverter, | |||
| $codec, | |||
| $this->timeConverter | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Builder; | |||
| use Ramsey\Uuid\Codec\CodecInterface; | |||
| use Ramsey\Uuid\Exception\BuilderNotFoundException; | |||
| use Ramsey\Uuid\Exception\UnableToBuildUuidException; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| /** | |||
| * FallbackBuilder builds a UUID by stepping through a list of UUID builders | |||
| * until a UUID can be constructed without exceptions | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class FallbackBuilder implements UuidBuilderInterface | |||
| { | |||
| /** | |||
| * @param iterable<UuidBuilderInterface> $builders An array of UUID builders | |||
| */ | |||
| public function __construct(private iterable $builders) | |||
| { | |||
| } | |||
| /** | |||
| * Builds and returns a UuidInterface instance using the first builder that | |||
| * succeeds | |||
| * | |||
| * @param CodecInterface $codec The codec to use for building this instance | |||
| * @param string $bytes The byte string from which to construct a UUID | |||
| * | |||
| * @return UuidInterface an instance of a UUID object | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public function build(CodecInterface $codec, string $bytes): UuidInterface | |||
| { | |||
| $lastBuilderException = null; | |||
| foreach ($this->builders as $builder) { | |||
| try { | |||
| return $builder->build($codec, $bytes); | |||
| } catch (UnableToBuildUuidException $exception) { | |||
| $lastBuilderException = $exception; | |||
| continue; | |||
| } | |||
| } | |||
| throw new BuilderNotFoundException( | |||
| 'Could not find a suitable builder for the provided codec and fields', | |||
| 0, | |||
| $lastBuilderException | |||
| ); | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Builder; | |||
| use Ramsey\Uuid\Codec\CodecInterface; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| /** | |||
| * A UUID builder builds instances of UuidInterface | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| interface UuidBuilderInterface | |||
| { | |||
| /** | |||
| * Builds and returns a UuidInterface | |||
| * | |||
| * @param CodecInterface $codec The codec to use for building this UuidInterface instance | |||
| * @param string $bytes The byte string from which to construct a UUID | |||
| * | |||
| * @return UuidInterface Implementations may choose to return more specific | |||
| * instances of UUIDs that implement UuidInterface | |||
| * | |||
| * @psalm-pure | |||
| */ | |||
| public function build(CodecInterface $codec, string $bytes): UuidInterface; | |||
| } | |||
| @@ -0,0 +1,71 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| /** | |||
| * A codec encodes and decodes a UUID according to defined rules | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| interface CodecInterface | |||
| { | |||
| /** | |||
| * Returns a hexadecimal string representation of a UuidInterface | |||
| * | |||
| * @param UuidInterface $uuid The UUID for which to create a hexadecimal | |||
| * string representation | |||
| * | |||
| * @return string Hexadecimal string representation of a UUID | |||
| * | |||
| * @psalm-return non-empty-string | |||
| */ | |||
| public function encode(UuidInterface $uuid): string; | |||
| /** | |||
| * Returns a binary string representation of a UuidInterface | |||
| * | |||
| * @param UuidInterface $uuid The UUID for which to create a binary string | |||
| * representation | |||
| * | |||
| * @return string Binary string representation of a UUID | |||
| * | |||
| * @psalm-return non-empty-string | |||
| */ | |||
| public function encodeBinary(UuidInterface $uuid): string; | |||
| /** | |||
| * Returns a UuidInterface derived from a hexadecimal string representation | |||
| * | |||
| * @param string $encodedUuid The hexadecimal string representation to | |||
| * convert into a UuidInterface instance | |||
| * | |||
| * @return UuidInterface An instance of a UUID decoded from a hexadecimal | |||
| * string representation | |||
| */ | |||
| public function decode(string $encodedUuid): UuidInterface; | |||
| /** | |||
| * Returns a UuidInterface derived from a binary string representation | |||
| * | |||
| * @param string $bytes The binary string representation to convert into a | |||
| * UuidInterface instance | |||
| * | |||
| * @return UuidInterface An instance of a UUID decoded from a binary string | |||
| * representation | |||
| */ | |||
| public function decodeBytes(string $bytes): UuidInterface; | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| use Ramsey\Uuid\Guid\Guid; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| use function bin2hex; | |||
| use function sprintf; | |||
| use function substr; | |||
| /** | |||
| * GuidStringCodec encodes and decodes globally unique identifiers (GUID) | |||
| * | |||
| * @see Guid | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class GuidStringCodec extends StringCodec | |||
| { | |||
| public function encode(UuidInterface $uuid): string | |||
| { | |||
| $hex = bin2hex($uuid->getFields()->getBytes()); | |||
| /** @var non-empty-string */ | |||
| return sprintf( | |||
| '%02s%02s%02s%02s-%02s%02s-%02s%02s-%04s-%012s', | |||
| substr($hex, 6, 2), | |||
| substr($hex, 4, 2), | |||
| substr($hex, 2, 2), | |||
| substr($hex, 0, 2), | |||
| substr($hex, 10, 2), | |||
| substr($hex, 8, 2), | |||
| substr($hex, 14, 2), | |||
| substr($hex, 12, 2), | |||
| substr($hex, 16, 4), | |||
| substr($hex, 20), | |||
| ); | |||
| } | |||
| public function decode(string $encodedUuid): UuidInterface | |||
| { | |||
| $bytes = $this->getBytes($encodedUuid); | |||
| return $this->getBuilder()->build($this, $this->swapBytes($bytes)); | |||
| } | |||
| public function decodeBytes(string $bytes): UuidInterface | |||
| { | |||
| // Specifically call parent::decode to preserve correct byte order | |||
| return parent::decode(bin2hex($bytes)); | |||
| } | |||
| /** | |||
| * Swaps bytes according to the GUID rules | |||
| */ | |||
| private function swapBytes(string $bytes): string | |||
| { | |||
| return $bytes[3] . $bytes[2] . $bytes[1] . $bytes[0] | |||
| . $bytes[5] . $bytes[4] | |||
| . $bytes[7] . $bytes[6] | |||
| . substr($bytes, 8); | |||
| } | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| use Ramsey\Uuid\Exception\InvalidArgumentException; | |||
| use Ramsey\Uuid\Exception\UnsupportedOperationException; | |||
| use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; | |||
| use Ramsey\Uuid\Uuid; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| use function strlen; | |||
| use function substr; | |||
| /** | |||
| * OrderedTimeCodec encodes and decodes a UUID, optimizing the byte order for | |||
| * more efficient storage | |||
| * | |||
| * For binary representations of version 1 UUID, this codec may be used to | |||
| * reorganize the time fields, making the UUID closer to sequential when storing | |||
| * the bytes. According to Percona, this optimization can improve database | |||
| * INSERTs and SELECTs using the UUID column as a key. | |||
| * | |||
| * The string representation of the UUID will remain unchanged. Only the binary | |||
| * representation is reordered. | |||
| * | |||
| * **PLEASE NOTE:** Binary representations of UUIDs encoded with this codec must | |||
| * be decoded with this codec. Decoding using another codec can result in | |||
| * malformed UUIDs. | |||
| * | |||
| * @link https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/ Storing UUID Values in MySQL | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class OrderedTimeCodec extends StringCodec | |||
| { | |||
| /** | |||
| * Returns a binary string representation of a UUID, with the timestamp | |||
| * fields rearranged for optimized storage | |||
| * | |||
| * @inheritDoc | |||
| * @psalm-return non-empty-string | |||
| * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty | |||
| * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty | |||
| */ | |||
| public function encodeBinary(UuidInterface $uuid): string | |||
| { | |||
| if ( | |||
| !($uuid->getFields() instanceof Rfc4122FieldsInterface) | |||
| || $uuid->getFields()->getVersion() !== Uuid::UUID_TYPE_TIME | |||
| ) { | |||
| throw new InvalidArgumentException( | |||
| 'Expected RFC 4122 version 1 (time-based) UUID' | |||
| ); | |||
| } | |||
| $bytes = $uuid->getFields()->getBytes(); | |||
| /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ | |||
| return $bytes[6] . $bytes[7] | |||
| . $bytes[4] . $bytes[5] | |||
| . $bytes[0] . $bytes[1] . $bytes[2] . $bytes[3] | |||
| . substr($bytes, 8); | |||
| } | |||
| /** | |||
| * Returns a UuidInterface derived from an ordered-time binary string | |||
| * representation | |||
| * | |||
| * @throws InvalidArgumentException if $bytes is an invalid length | |||
| * | |||
| * @inheritDoc | |||
| */ | |||
| public function decodeBytes(string $bytes): UuidInterface | |||
| { | |||
| if (strlen($bytes) !== 16) { | |||
| throw new InvalidArgumentException( | |||
| '$bytes string should contain 16 characters.' | |||
| ); | |||
| } | |||
| // Rearrange the bytes to their original order. | |||
| $rearrangedBytes = $bytes[4] . $bytes[5] . $bytes[6] . $bytes[7] | |||
| . $bytes[2] . $bytes[3] | |||
| . $bytes[0] . $bytes[1] | |||
| . substr($bytes, 8); | |||
| $uuid = parent::decodeBytes($rearrangedBytes); | |||
| if ( | |||
| !($uuid->getFields() instanceof Rfc4122FieldsInterface) | |||
| || $uuid->getFields()->getVersion() !== Uuid::UUID_TYPE_TIME | |||
| ) { | |||
| throw new UnsupportedOperationException( | |||
| 'Attempting to decode a non-time-based UUID using ' | |||
| . 'OrderedTimeCodec' | |||
| ); | |||
| } | |||
| return $uuid; | |||
| } | |||
| } | |||
| @@ -0,0 +1,131 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| use Ramsey\Uuid\Builder\UuidBuilderInterface; | |||
| use Ramsey\Uuid\Exception\InvalidArgumentException; | |||
| use Ramsey\Uuid\Exception\InvalidUuidStringException; | |||
| use Ramsey\Uuid\Uuid; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| use function bin2hex; | |||
| use function hex2bin; | |||
| use function implode; | |||
| use function sprintf; | |||
| use function str_replace; | |||
| use function strlen; | |||
| use function substr; | |||
| /** | |||
| * StringCodec encodes and decodes RFC 4122 UUIDs | |||
| * | |||
| * @link http://tools.ietf.org/html/rfc4122 | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class StringCodec implements CodecInterface | |||
| { | |||
| /** | |||
| * Constructs a StringCodec | |||
| * | |||
| * @param UuidBuilderInterface $builder The builder to use when encoding UUIDs | |||
| */ | |||
| public function __construct(private UuidBuilderInterface $builder) | |||
| { | |||
| } | |||
| public function encode(UuidInterface $uuid): string | |||
| { | |||
| $hex = bin2hex($uuid->getFields()->getBytes()); | |||
| /** @var non-empty-string */ | |||
| return sprintf( | |||
| '%08s-%04s-%04s-%04s-%012s', | |||
| substr($hex, 0, 8), | |||
| substr($hex, 8, 4), | |||
| substr($hex, 12, 4), | |||
| substr($hex, 16, 4), | |||
| substr($hex, 20), | |||
| ); | |||
| } | |||
| /** | |||
| * @psalm-return non-empty-string | |||
| * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty | |||
| * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty | |||
| */ | |||
| public function encodeBinary(UuidInterface $uuid): string | |||
| { | |||
| /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ | |||
| return $uuid->getFields()->getBytes(); | |||
| } | |||
| /** | |||
| * @throws InvalidUuidStringException | |||
| * | |||
| * @inheritDoc | |||
| */ | |||
| public function decode(string $encodedUuid): UuidInterface | |||
| { | |||
| return $this->builder->build($this, $this->getBytes($encodedUuid)); | |||
| } | |||
| public function decodeBytes(string $bytes): UuidInterface | |||
| { | |||
| if (strlen($bytes) !== 16) { | |||
| throw new InvalidArgumentException( | |||
| '$bytes string should contain 16 characters.' | |||
| ); | |||
| } | |||
| return $this->builder->build($this, $bytes); | |||
| } | |||
| /** | |||
| * Returns the UUID builder | |||
| */ | |||
| protected function getBuilder(): UuidBuilderInterface | |||
| { | |||
| return $this->builder; | |||
| } | |||
| /** | |||
| * Returns a byte string of the UUID | |||
| */ | |||
| protected function getBytes(string $encodedUuid): string | |||
| { | |||
| $parsedUuid = str_replace( | |||
| ['urn:', 'uuid:', 'URN:', 'UUID:', '{', '}', '-'], | |||
| '', | |||
| $encodedUuid | |||
| ); | |||
| $components = [ | |||
| substr($parsedUuid, 0, 8), | |||
| substr($parsedUuid, 8, 4), | |||
| substr($parsedUuid, 12, 4), | |||
| substr($parsedUuid, 16, 4), | |||
| substr($parsedUuid, 20), | |||
| ]; | |||
| if (!Uuid::isValid(implode('-', $components))) { | |||
| throw new InvalidUuidStringException( | |||
| 'Invalid UUID string: ' . $encodedUuid | |||
| ); | |||
| } | |||
| return (string) hex2bin($parsedUuid); | |||
| } | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| use Ramsey\Uuid\Exception\InvalidUuidStringException; | |||
| use Ramsey\Uuid\UuidInterface; | |||
| use function bin2hex; | |||
| use function sprintf; | |||
| use function substr; | |||
| use function substr_replace; | |||
| /** | |||
| * TimestampFirstCombCodec encodes and decodes COMBs, with the timestamp as the | |||
| * first 48 bits | |||
| * | |||
| * In contrast with the TimestampLastCombCodec, the TimestampFirstCombCodec | |||
| * adds the timestamp to the first 48 bits of the COMB. To generate a | |||
| * timestamp-first COMB, set the TimestampFirstCombCodec as the codec, along | |||
| * with the CombGenerator as the random generator. | |||
| * | |||
| * ``` php | |||
| * $factory = new UuidFactory(); | |||
| * | |||
| * $factory->setCodec(new TimestampFirstCombCodec($factory->getUuidBuilder())); | |||
| * | |||
| * $factory->setRandomGenerator(new CombGenerator( | |||
| * $factory->getRandomGenerator(), | |||
| * $factory->getNumberConverter() | |||
| * )); | |||
| * | |||
| * $timestampFirstComb = $factory->uuid4(); | |||
| * ``` | |||
| * | |||
| * @link https://www.informit.com/articles/printerfriendly/25862 The Cost of GUIDs as Primary Keys | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class TimestampFirstCombCodec extends StringCodec | |||
| { | |||
| /** | |||
| * @psalm-return non-empty-string | |||
| * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty | |||
| * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty | |||
| */ | |||
| public function encode(UuidInterface $uuid): string | |||
| { | |||
| $bytes = $this->swapBytes($uuid->getFields()->getBytes()); | |||
| return sprintf( | |||
| '%08s-%04s-%04s-%04s-%012s', | |||
| bin2hex(substr($bytes, 0, 4)), | |||
| bin2hex(substr($bytes, 4, 2)), | |||
| bin2hex(substr($bytes, 6, 2)), | |||
| bin2hex(substr($bytes, 8, 2)), | |||
| bin2hex(substr($bytes, 10)) | |||
| ); | |||
| } | |||
| /** | |||
| * @psalm-return non-empty-string | |||
| * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty | |||
| * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty | |||
| */ | |||
| public function encodeBinary(UuidInterface $uuid): string | |||
| { | |||
| /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ | |||
| return $this->swapBytes($uuid->getFields()->getBytes()); | |||
| } | |||
| /** | |||
| * @throws InvalidUuidStringException | |||
| * | |||
| * @inheritDoc | |||
| */ | |||
| public function decode(string $encodedUuid): UuidInterface | |||
| { | |||
| $bytes = $this->getBytes($encodedUuid); | |||
| return $this->getBuilder()->build($this, $this->swapBytes($bytes)); | |||
| } | |||
| public function decodeBytes(string $bytes): UuidInterface | |||
| { | |||
| return $this->getBuilder()->build($this, $this->swapBytes($bytes)); | |||
| } | |||
| /** | |||
| * Swaps bytes according to the timestamp-first COMB rules | |||
| */ | |||
| private function swapBytes(string $bytes): string | |||
| { | |||
| $first48Bits = substr($bytes, 0, 6); | |||
| $last48Bits = substr($bytes, -6); | |||
| $bytes = substr_replace($bytes, $last48Bits, 0, 6); | |||
| $bytes = substr_replace($bytes, $first48Bits, -6); | |||
| return $bytes; | |||
| } | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Codec; | |||
| /** | |||
| * TimestampLastCombCodec encodes and decodes COMBs, with the timestamp as the | |||
| * last 48 bits | |||
| * | |||
| * The CombGenerator when used with the StringCodec (and, by proxy, the | |||
| * TimestampLastCombCodec) adds the timestamp to the last 48 bits of the COMB. | |||
| * The TimestampLastCombCodec is provided for the sake of consistency. In | |||
| * practice, it is identical to the standard StringCodec but, it may be used | |||
| * with the CombGenerator for additional context when reading code. | |||
| * | |||
| * Consider the following code. By default, the codec used by UuidFactory is the | |||
| * StringCodec, but here, we explicitly set the TimestampLastCombCodec. It is | |||
| * redundant, but it is clear that we intend this COMB to be generated with the | |||
| * timestamp appearing at the end. | |||
| * | |||
| * ``` php | |||
| * $factory = new UuidFactory(); | |||
| * | |||
| * $factory->setCodec(new TimestampLastCombCodec($factory->getUuidBuilder())); | |||
| * | |||
| * $factory->setRandomGenerator(new CombGenerator( | |||
| * $factory->getRandomGenerator(), | |||
| * $factory->getNumberConverter() | |||
| * )); | |||
| * | |||
| * $timestampLastComb = $factory->uuid4(); | |||
| * ``` | |||
| * | |||
| * @link https://www.informit.com/articles/printerfriendly/25862 The Cost of GUIDs as Primary Keys | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class TimestampLastCombCodec extends StringCodec | |||
| { | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| <?php | |||
| /** | |||
| * This file is part of the ramsey/uuid library | |||
| * | |||
| * For the full copyright and license information, please view the LICENSE | |||
| * file that was distributed with this source code. | |||
| * | |||
| * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> | |||
| * @license http://opensource.org/licenses/MIT MIT | |||
| */ | |||
| declare(strict_types=1); | |||
| namespace Ramsey\Uuid\Converter\Number; | |||
| use Ramsey\Uuid\Converter\NumberConverterInterface; | |||
| use Ramsey\Uuid\Math\BrickMathCalculator; | |||
| /** | |||
| * Previously used to integrate moontoast/math as a bignum arithmetic library, | |||
| * BigNumberConverter is deprecated in favor of GenericNumberConverter | |||
| * | |||
| * @deprecated Transition to {@see GenericNumberConverter}. | |||
| * | |||
| * @psalm-immutable | |||
| */ | |||
| class BigNumberConverter implements NumberConverterInterface | |||
| { | |||
| private NumberConverterInterface $converter; | |||
| public function __construct() | |||
| { | |||
| $this->converter = new GenericNumberConverter(new BrickMathCalculator()); | |||
| } | |||
| /** | |||
| * @inheritDoc | |||
| * @psalm-pure | |||
| */ | |||
| public function fromHex(string $hex): string | |||
| { | |||
| return $this->converter->fromHex($hex); | |||
| } | |||
| /** | |||
| * @inheritDoc | |||
| * @psalm-pure | |||
| */ | |||
| public function toHex(string $number): string | |||
| { | |||
| return $this->converter->toHex($number); | |||
| } | |||
| } | |||