From febcc20001e54d4dcbc9a6b94afd5f7d061de7f1 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Wed, 4 Oct 2023 23:57:10 +0000 Subject: [PATCH 01/11] improve grammar performance --- DataStorage/Database/GrammarAbstract.php | 25 ++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/DataStorage/Database/GrammarAbstract.php b/DataStorage/Database/GrammarAbstract.php index ab34a8603..7b27e7863 100755 --- a/DataStorage/Database/GrammarAbstract.php +++ b/DataStorage/Database/GrammarAbstract.php @@ -226,16 +226,16 @@ abstract class GrammarAbstract $identifierStart = $this->systemIdentifierStart; $identifierEnd = $this->systemIdentifierEnd; - if (\stripos($system, '(') !== false) { - $identifierStart = ''; - $identifierEnd = ''; - } + // @todo: maybe the if/elseif has to get swapped in order. There could be a count(table.name) for example + if ((\stripos($system, '.')) !== false) { + // The following code could have been handled with \explode more elegantly but \explode needs more memory and more time + // Normally this wouldn't be a problem but in this case there are so many function calls to this routine, + // that it makes sense to make this "minor" improvement. - // The following code could have been handled with \explode more elegantly but \explode needs more memory and more time - // Normally this wouldn't be a problem but in this case there are so many function calls to this routine, - // that it makes sense to make this "minor" improvement. - if (($pos = \stripos($system, '.')) !== false) { - $split = [\substr($system, 0, $pos), \substr($system, $pos + 1)]; + // This is actually slower than \explode(), despite knowing the first index + //$split = [\substr($system, 0, $pos), \substr($system, $pos + 1)]; + + $split = \explode('.', $system); $identifierTwoStart = $identifierStart; $identifierTwoEnd = $identifierEnd; @@ -248,9 +248,10 @@ abstract class GrammarAbstract return $identifierStart . $split[0] . $identifierEnd . '.' . $identifierTwoStart . $split[1] . $identifierTwoEnd; - } - - if ($system === '*') { + } elseif ($system === '*' + || \stripos($system, '(') !== false + || \is_numeric($system) + ) { $identifierStart = ''; $identifierEnd = ''; } From e873f909fda84b37e6bd9dec9f2f0b45fbd3cb5e Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Wed, 4 Oct 2023 23:57:30 +0000 Subject: [PATCH 02/11] add exists functionality (similar to count but just checks existence) --- .../Database/Mapper/DataMapperFactory.php | 14 ++++++++ DataStorage/Database/Mapper/MapperType.php | 2 ++ DataStorage/Database/Mapper/ReadMapper.php | 32 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/DataStorage/Database/Mapper/DataMapperFactory.php b/DataStorage/Database/Mapper/DataMapperFactory.php index 18988b8bd..8eb0bd878 100755 --- a/DataStorage/Database/Mapper/DataMapperFactory.php +++ b/DataStorage/Database/Mapper/DataMapperFactory.php @@ -254,6 +254,20 @@ class DataMapperFactory return (new ReadMapper(new static(), $db ?? self::$db))->count(); } + /** + * Create read mapper + * + * @param ConnectionAbstract $db Database connection + * + * @return ReadMapper + * + * @since 1.0.0 + */ + public static function exists(ConnectionAbstract $db = null) : ReadMapper + { + return (new ReadMapper(new static(), $db ?? self::$db))->exists(); + } + /** * Create read mapper * diff --git a/DataStorage/Database/Mapper/MapperType.php b/DataStorage/Database/Mapper/MapperType.php index 90ab70552..b25f24598 100755 --- a/DataStorage/Database/Mapper/MapperType.php +++ b/DataStorage/Database/Mapper/MapperType.php @@ -38,6 +38,8 @@ abstract class MapperType extends Enum public const COUNT_MODELS = 12; + public const MODEL_EXISTS = 13; + // -------------------------------------------- // public const CREATE = 1001; diff --git a/DataStorage/Database/Mapper/ReadMapper.php b/DataStorage/Database/Mapper/ReadMapper.php index e67844987..39b2a604c 100755 --- a/DataStorage/Database/Mapper/ReadMapper.php +++ b/DataStorage/Database/Mapper/ReadMapper.php @@ -102,6 +102,20 @@ final class ReadMapper extends DataMapperAbstract return $this; } + /** + * Create count mapper + * + * @return self + * + * @since 1.0.0 + */ + public function exists() : self + { + $this->type = MapperType::MODEL_EXISTS; + + return $this; + } + /** * Create random mapper * @@ -158,6 +172,8 @@ final class ReadMapper extends DataMapperAbstract return $this->executeGetRaw(); case MapperType::COUNT_MODELS: return $this->executeCount(); + case MapperType::MODEL_EXISTS: + return $this->executeExists(); default: return null; } @@ -227,7 +243,7 @@ final class ReadMapper extends DataMapperAbstract try { $results = false; - $sth = $this->db->con->prepare($a = $query->toSql()); + $sth = $this->db->con->prepare($query->toSql()); if ($sth !== false) { $sth->execute(); $results = $sth->fetchAll(\PDO::FETCH_ASSOC); @@ -285,6 +301,20 @@ final class ReadMapper extends DataMapperAbstract return (int) $query->execute()?->fetchColumn(); } + /** + * Check if any element exists + * + * @return int + * + * @since 1.0.0 + */ + public function executeExists() : bool + { + $query = $this->getQuery(null, ['1']); + + return ($query->execute()?->fetchColumn() ?? 0) > 0; + } + /** * Get random object * From cd9a5cdc11a81d6fd2f985eb7914d10d4d44df69 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 5 Oct 2023 10:12:56 +0000 Subject: [PATCH 03/11] fix bug and add query yield --- DataStorage/Database/GrammarAbstract.php | 25 +- .../Database/Mapper/DataMapperFactory.php | 31 +++ DataStorage/Database/Mapper/MapperType.php | 4 + DataStorage/Database/Mapper/ReadMapper.php | 233 +++++++++++++++++- DataStorage/Database/Query/Builder.php | 6 +- 5 files changed, 272 insertions(+), 27 deletions(-) diff --git a/DataStorage/Database/GrammarAbstract.php b/DataStorage/Database/GrammarAbstract.php index 7b27e7863..3bf1d8c43 100755 --- a/DataStorage/Database/GrammarAbstract.php +++ b/DataStorage/Database/GrammarAbstract.php @@ -202,7 +202,9 @@ abstract class GrammarAbstract $expression .= $element() . (\is_string($key) ? ' as ' . $key : '') . ', '; } elseif ($element instanceof BuilderAbstract) { $expression .= $element->toSql() . (\is_string($key) ? ' as ' . $key : '') . ', '; - } else { + } elseif (\is_int($element)) { + $expression .= $element . ', '; + }else { throw new \InvalidArgumentException(); } } @@ -226,15 +228,18 @@ abstract class GrammarAbstract $identifierStart = $this->systemIdentifierStart; $identifierEnd = $this->systemIdentifierEnd; - // @todo: maybe the if/elseif has to get swapped in order. There could be a count(table.name) for example - if ((\stripos($system, '.')) !== false) { - // The following code could have been handled with \explode more elegantly but \explode needs more memory and more time - // Normally this wouldn't be a problem but in this case there are so many function calls to this routine, - // that it makes sense to make this "minor" improvement. - + // The order of this if/elseif statement is important!!! + if ($system === '*' + || \stripos($system, '(') !== false + || \is_numeric($system) + ) { + $identifierStart = ''; + $identifierEnd = ''; + } elseif ((\stripos($system, '.')) !== false) { // This is actually slower than \explode(), despite knowing the first index //$split = [\substr($system, 0, $pos), \substr($system, $pos + 1)]; + // Faster! But might requires more memory? $split = \explode('.', $system); $identifierTwoStart = $identifierStart; @@ -248,12 +253,6 @@ abstract class GrammarAbstract return $identifierStart . $split[0] . $identifierEnd . '.' . $identifierTwoStart . $split[1] . $identifierTwoEnd; - } elseif ($system === '*' - || \stripos($system, '(') !== false - || \is_numeric($system) - ) { - $identifierStart = ''; - $identifierEnd = ''; } return $identifierStart . $system . $identifierEnd; diff --git a/DataStorage/Database/Mapper/DataMapperFactory.php b/DataStorage/Database/Mapper/DataMapperFactory.php index 8eb0bd878..9399abfd6 100755 --- a/DataStorage/Database/Mapper/DataMapperFactory.php +++ b/DataStorage/Database/Mapper/DataMapperFactory.php @@ -209,6 +209,23 @@ class DataMapperFactory return $reader->get(); } + /** + * Create read mapper + * + * @param ConnectionAbstract $db Database connection + * + * @return ReadMapper + * + * @since 1.0.0 + */ + public static function yield(ConnectionAbstract $db = null) : ReadMapper + { + /** @var ReadMapper $reader */ + $reader = new ReadMapper(new static(), $db ?? self::$db); + + return $reader->yield(); + } + /** * Create read mapper * @@ -268,6 +285,20 @@ class DataMapperFactory return (new ReadMapper(new static(), $db ?? self::$db))->exists(); } + /** + * Create read mapper + * + * @param ConnectionAbstract $db Database connection + * + * @return ReadMapper + * + * @since 1.0.0 + */ + public static function has(ConnectionAbstract $db = null) : ReadMapper + { + return (new ReadMapper(new static(), $db ?? self::$db))->has(); + } + /** * Create read mapper * diff --git a/DataStorage/Database/Mapper/MapperType.php b/DataStorage/Database/Mapper/MapperType.php index b25f24598..c7fa29c8f 100755 --- a/DataStorage/Database/Mapper/MapperType.php +++ b/DataStorage/Database/Mapper/MapperType.php @@ -28,6 +28,8 @@ abstract class MapperType extends Enum { public const GET = 1; + public const GET_YIELD = 2; + public const GET_ALL = 4; public const FIND = 7; @@ -40,6 +42,8 @@ abstract class MapperType extends Enum public const MODEL_EXISTS = 13; + public const MODEL_HAS_RELATION = 14; + // -------------------------------------------- // public const CREATE = 1001; diff --git a/DataStorage/Database/Mapper/ReadMapper.php b/DataStorage/Database/Mapper/ReadMapper.php index 39b2a604c..812057805 100755 --- a/DataStorage/Database/Mapper/ReadMapper.php +++ b/DataStorage/Database/Mapper/ReadMapper.php @@ -58,6 +58,22 @@ final class ReadMapper extends DataMapperAbstract return $this; } + /** + * Create yield mapper + * + * This makes execute() return a single object or an array of object depending the result size + * + * @return self + * + * @since 1.0.0 + */ + public function yield() : self + { + $this->type = MapperType::GET_YIELD; + + return $this; + } + /** * Get raw result set * @@ -103,7 +119,7 @@ final class ReadMapper extends DataMapperAbstract } /** - * Create count mapper + * Create exists mapper * * @return self * @@ -116,6 +132,20 @@ final class ReadMapper extends DataMapperAbstract return $this; } + /** + * Create has mapper + * + * @return self + * + * @since 1.0.0 + */ + public function has() : self + { + $this->type = MapperType::MODEL_HAS_RELATION; + + return $this; + } + /** * Create random mapper * @@ -162,6 +192,9 @@ final class ReadMapper extends DataMapperAbstract case MapperType::GET: /** @var null|Builder ...$options */ return $this->executeGet(...$options); + case MapperType::GET_YIELD: + /** @var null|Builder ...$options */ + return $this->executeGetYield(...$options); case MapperType::GET_RAW: /** @var null|Builder ...$options */ return $this->executeGetRaw(...$options); @@ -174,6 +207,8 @@ final class ReadMapper extends DataMapperAbstract return $this->executeCount(); case MapperType::MODEL_EXISTS: return $this->executeExists(); + case MapperType::MODEL_HAS_RELATION: + return $this->executeHas(); default: return null; } @@ -204,9 +239,13 @@ final class ReadMapper extends DataMapperAbstract $obj = []; // Get remaining objects (not available in memory cache) or remaining where clauses. - $dbData = $this->executeGetRaw($query); + //$dbData = $this->executeGetRaw($query); + + foreach ($this->executeGetRawYield($query) as $row) { + if ($row === []) { + continue; + } - foreach ($dbData as $row) { $value = $row[$this->mapper::PRIMARYFIELD . '_d' . $this->depth]; $obj[$value] = $this->mapper::createBaseModel($row); @@ -225,6 +264,34 @@ final class ReadMapper extends DataMapperAbstract return $obj; } + /** + * Execute mapper + * + * @param null|Builder $query Query to use instead of the internally generated query + * Careful, this doesn't merge with the internal query. + * If you want to merge it use ->query() instead + * + * @since 1.0.0 + */ + public function executeGetYield(Builder $query = null) + { + $primaryKeys = []; + $memberOfPrimaryField = $this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']; + + if (isset($this->where[$memberOfPrimaryField])) { + $keys = $this->where[$memberOfPrimaryField][0]['value']; + $primaryKeys = \array_merge(\is_array($keys) ? $keys : [$keys], $primaryKeys); + } + + foreach ($this->executeGetRawYield($query) as $row) { + $obj = $this->mapper::createBaseModel($row); + $obj = $this->populateAbstract($row, $obj); + $this->loadHasManyRelations($obj); + + yield $obj; + } + } + /** * Execute mapper * @@ -263,6 +330,47 @@ final class ReadMapper extends DataMapperAbstract return $results === false ? [] : $results; } + /** + * Execute mapper + * + * @param null|Builder $query Query to use instead of the internally generated query + * Careful, this doesn't merge with the internal query. + * If you want to merge it use ->query() instead + * + * @return array + * + * @since 1.0.0 + */ + public function executeGetRawYield(Builder $query = null) + { + $query ??= $this->getQuery(); + + try { + $sth = $this->db->con->prepare($query->toSql()); + if ($sth === false) { + yield []; + + return; + } + + $sth->execute(); + + while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) { + yield $row; + } + } catch (\Throwable $t) { + \phpOMS\Log\FileLogger::getInstance()->error( + \phpOMS\Log\FileLogger::MSG_FULL, [ + 'message' => $t->getMessage() . ':' . $query->toSql(), + 'line' => __LINE__, + 'file' => self::class, + ] + ); + + yield []; + } + } + /** * Execute mapper * @@ -304,17 +412,33 @@ final class ReadMapper extends DataMapperAbstract /** * Check if any element exists * - * @return int + * @return bool * * @since 1.0.0 */ public function executeExists() : bool { - $query = $this->getQuery(null, ['1']); + $query = $this->getQuery(null, [1]); return ($query->execute()?->fetchColumn() ?? 0) > 0; } + /** + * Check if any element exists + * + * @return bool + * + * @since 1.0.0 + */ + public function executeHas() : bool + { + $obj = isset($this->where[$this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']]) + ? $this->mapper::createNullModel($this->where[$this->mapper::COLUMNS[$this->mapper::PRIMARYFIELD]['internal']][0]['value']) + : $this->columns([1])->executeGet(); + + return $this->hasManyRelations($obj); + } + /** * Get random object * @@ -348,10 +472,18 @@ final class ReadMapper extends DataMapperAbstract : $columns; foreach ($columns as $key => $values) { - if (\is_string($values)) { - $query->selectAs($key, $values); + if (\is_string($values) || \is_int($values)) { + if (\is_int($key)) { + $query->select($values); + } else { + $query->selectAs($key, $values); + } } elseif (($values['writeonly'] ?? false) === false || isset($this->with[$values['internal']])) { - $query->selectAs($this->mapper::TABLE . '_d' . $this->depth . '.' . $key, $key . '_d' . $this->depth); + if (\is_int($key)) { + $query->select($key); + } else { + $query->selectAs($this->mapper::TABLE . '_d' . $this->depth . '.' . $key, $key . '_d' . $this->depth); + } } } @@ -472,7 +604,7 @@ final class ReadMapper extends DataMapperAbstract $where1->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, $comparison, $where['value'], 'and'); $where2 = new Builder($this->db); - $where2->select('1') + $where2->select('1') // @todo: why is this in quotes? ->from($this->mapper::TABLE . '_d' . $this->depth) ->where($this->mapper::TABLE . '_d' . $this->depth . '.' . $col, 'in', $alt); @@ -991,4 +1123,87 @@ final class ReadMapper extends DataMapperAbstract } } } + + /** + * Checks if object has certain relations + * + * @param object $obj Object to check + * + * @return bool + * + * @since 1.0.0 + */ + public function hasManyRelations(object $obj) : bool + { + if (empty($this->with)) { + return true; + } + + $primaryKey = $this->mapper::getObjectId($obj); + if (empty($primaryKey)) { + return false; + } + + $refClass = null; + + // @todo: check if there are more cases where the relation is already loaded with joins etc. + // there can be pseudo has many elements like localizations. They are has manies but these are already loaded with joins! + foreach ($this->with as $member => $withData) { + if (isset($this->mapper::HAS_MANY[$member])) { + $many = $this->mapper::HAS_MANY[$member]; + if (isset($many['column'])) { + continue; + } + + // @todo: withData doesn't store this directly, it is in [0]['private] ?!?! + $isPrivate = $withData['private'] ?? false; + + $objectMapper = $this->createRelationMapper($many['mapper']::exists(db: $this->db), $member); + if ($many['external'] === null/* same as $many['table'] !== $many['mapper']::TABLE */) { + $objectMapper->where($many['mapper']::COLUMNS[$many['self']]['internal'], $primaryKey); + } else { + $query = new Builder($this->db, true); + $query->leftJoin($many['table']) + ->on($many['mapper']::TABLE . '_d1.' . $many['mapper']::PRIMARYFIELD, '=', $many['table'] . '.' . $many['external']) + ->where($many['table'] . '.' . $many['self'], '=', $primaryKey); + + // Cannot use join, because join only works on members and we don't have members for a relation table + // This is why we need to create a "base" query which contians the join on table columns + $objectMapper->query($query); + } + + $objects = $objectMapper->execute(); + if (empty($objects) || $objects === false) { + return false; + } + + return true; + } elseif (isset($this->mapper::OWNS_ONE[$member]) + || isset($this->mapper::BELONGS_TO[$member]) + ) { + $relation = isset($this->mapper::OWNS_ONE[$member]) + ? $this->mapper::OWNS_ONE[$member] + : $this->mapper::BELONGS_TO[$member]; + + if (\count($withData) < 2) { + continue; + } + + /** @var ReadMapper $relMapper */ + $relMapper = $this->createRelationMapper($relation['mapper']::reader($this->db), $member); + + $isPrivate = $withData['private'] ?? false; + if ($isPrivate) { + if ($refClass === null) { + $refClass = new \ReflectionClass($obj); + } + + $refProp = $refClass->getProperty($member); + return $relMapper->hasManyRelations($refProp->getValue($obj)); + } else { + return $relMapper->hasManyRelations($obj->{$member}); + } + } + } + } } diff --git a/DataStorage/Database/Query/Builder.php b/DataStorage/Database/Query/Builder.php index 482cbfa6c..6356e706d 100755 --- a/DataStorage/Database/Query/Builder.php +++ b/DataStorage/Database/Query/Builder.php @@ -280,11 +280,7 @@ class Builder extends BuilderAbstract /** @var mixed[] $columns */ /** @var mixed $column */ foreach ($columns as $column) { - if (\is_string($column) || $column instanceof self) { - $this->selects[] = $column; - } else { - throw new \InvalidArgumentException(); - } + $this->selects[] = $column; } return $this; From 007fc6dfe87656b8463367efb056b535e583833c Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 5 Oct 2023 10:13:08 +0000 Subject: [PATCH 04/11] fix status code --- Message/Http/HttpHeader.php | 118 ++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/Message/Http/HttpHeader.php b/Message/Http/HttpHeader.php index 9b001601b..100857e5c 100755 --- a/Message/Http/HttpHeader.php +++ b/Message/Http/HttpHeader.php @@ -344,241 +344,241 @@ final class HttpHeader extends HeaderAbstract switch ($code) { case RequestStatusCode::R_100: $this->set('', 'HTTP/1.0 100 Continue', true); - $this->set('Status', '100 Continue', true); + $this->set('Status', '100', true); break; case RequestStatusCode::R_101: $this->set('', 'HTTP/1.0 101 Switching protocols', true); - $this->set('Status', '101 Switching protocols', true); + $this->set('Status', '101', true); break; case RequestStatusCode::R_102: $this->set('', 'HTTP/1.0 102 Processing', true); - $this->set('Status', '102 Processing', true); + $this->set('Status', '102', true); break; case RequestStatusCode::R_200: $this->set('', 'HTTP/1.0 200 OK', true); - $this->set('Status', '200 OK', true); + $this->set('Status', '200', true); break; case RequestStatusCode::R_201: $this->set('', 'HTTP/1.0 201 Created', true); - $this->set('Status', '201 Created', true); + $this->set('Status', '201', true); break; case RequestStatusCode::R_202: $this->set('', 'HTTP/1.0 202 Accepted', true); - $this->set('Status', '202 Accepted', true); + $this->set('Status', '202', true); break; case RequestStatusCode::R_203: $this->set('', 'HTTP/1.0 203 Non-Authoritative Information', true); - $this->set('Status', '203 Non-Authoritative Information', true); + $this->set('Status', '203', true); break; case RequestStatusCode::R_204: $this->set('', 'HTTP/1.0 204 No Content', true); - $this->set('Status', '204 No Content', true); + $this->set('Status', '204', true); break; case RequestStatusCode::R_205: $this->set('', 'HTTP/1.0 205 Reset Content', true); - $this->set('Status', '205 Reset Content', true); + $this->set('Status', '205', true); break; case RequestStatusCode::R_206: $this->set('', 'HTTP/1.0 206 Partial Content', true); - $this->set('Status', '206 Partial Content', true); + $this->set('Status', '206', true); break; case RequestStatusCode::R_207: $this->set('', 'HTTP/1.0 207 Multi-Status', true); - $this->set('Status', '207 Multi-Status', true); + $this->set('Status', '207', true); break; case RequestStatusCode::R_300: $this->set('', 'HTTP/1.0 300 Multiple Choices', true); - $this->set('Status', '300 Multiple Choices', true); + $this->set('Status', '300', true); break; case RequestStatusCode::R_301: $this->set('', 'HTTP/1.0 301 Moved Permanently', true); - $this->set('Status', '301 Moved Permanently', true); + $this->set('Status', '301', true); break; case RequestStatusCode::R_302: $this->set('', 'HTTP/1.0 302 Found', true); - $this->set('Status', '302 Found', true); + $this->set('Status', '302', true); break; case RequestStatusCode::R_303: $this->set('', 'HTTP/1.0 303 See Other', true); - $this->set('Status', '303 See Other', true); + $this->set('Status', '303', true); break; case RequestStatusCode::R_304: $this->set('', 'HTTP/1.0 304 Not Modified', true); - $this->set('Status', '304 Not Modified', true); + $this->set('Status', '304', true); break; case RequestStatusCode::R_305: $this->set('', 'HTTP/1.0 305 Use Proxy', true); - $this->set('Status', '305 Use Proxy', true); + $this->set('Status', '305', true); break; case RequestStatusCode::R_306: $this->set('', 'HTTP/1.0 306 Switch Proxy', true); - $this->set('Status', '306 Switch Proxy', true); + $this->set('Status', '306', true); break; case RequestStatusCode::R_307: $this->set('', 'HTTP/1.0 307 Temporary Redirect', true); - $this->set('Status', '307 Temporary Redirect', true); + $this->set('Status', '307', true); break; case RequestStatusCode::R_308: $this->set('', 'HTTP/1.0 308 Permanent Redirect', true); - $this->set('Status', '308 Permanent Redirect', true); + $this->set('Status', '308', true); break; case RequestStatusCode::R_400: $this->set('', 'HTTP/1.0 400 Bad Request', true); - $this->set('Status', '400 Bad Request', true); + $this->set('Status', '400', true); break; case RequestStatusCode::R_401: $this->set('', 'HTTP/1.0 401 Unauthorized', true); - $this->set('Status', '401 Unauthorized', true); + $this->set('Status', '401', true); break; case RequestStatusCode::R_402: $this->set('', 'HTTP/1.0 402 Payment Required', true); - $this->set('Status', '402 Payment Required', true); + $this->set('Status', '402', true); break; case RequestStatusCode::R_403: $this->set('', 'HTTP/1.0 403 Forbidden', true); - $this->set('Status', '403 Forbidden', true); + $this->set('Status', '403', true); break; case RequestStatusCode::R_404: $this->set('', 'HTTP/1.0 404 Not Found', true); - $this->set('Status', '404 Not Found', true); + $this->set('Status', '404', true); break; case RequestStatusCode::R_405: $this->set('', 'HTTP/1.0 405 Method Not Allowed', true); - $this->set('Status', '405 Method Not Allowed', true); + $this->set('Status', '405', true); break; case RequestStatusCode::R_406: $this->set('', 'HTTP/1.0 406 Not acceptable', true); - $this->set('Status', '406 Not acceptable', true); + $this->set('Status', '406', true); break; case RequestStatusCode::R_407: $this->set('', 'HTTP/1.0 407 Proxy Authentication Required', true); - $this->set('Status', '407 Proxy Authentication Required', true); + $this->set('Status', '407', true); break; case RequestStatusCode::R_408: $this->set('', 'HTTP/1.0 408 Request Timeout', true); - $this->set('Status', '408 Request Timeout', true); + $this->set('Status', '408', true); break; case RequestStatusCode::R_409: $this->set('', 'HTTP/1.0 409 Conflict', true); - $this->set('Status', '409 Conflict', true); + $this->set('Status', '409', true); break; case RequestStatusCode::R_410: $this->set('', 'HTTP/1.0 410 Gone', true); - $this->set('Status', '410 Gone', true); + $this->set('Status', '410', true); break; case RequestStatusCode::R_411: $this->set('', 'HTTP/1.0 411 Length Required', true); - $this->set('Status', '411 Length Required', true); + $this->set('Status', '411', true); break; case RequestStatusCode::R_412: $this->set('', 'HTTP/1.0 412 Precondition Failed', true); - $this->set('Status', '412 Precondition Failed', true); + $this->set('Status', '412', true); break; case RequestStatusCode::R_413: $this->set('', 'HTTP/1.0 413 Request Entity Too Large', true); - $this->set('Status', '413 Request Entity Too Large', true); + $this->set('Status', '413', true); break; case RequestStatusCode::R_414: $this->set('', 'HTTP/1.0 414 Request-URI Too Long', true); - $this->set('Status', '414 Request-URI Too Long', true); + $this->set('Status', '414', true); break; case RequestStatusCode::R_415: $this->set('', 'HTTP/1.0 415 Unsupported Media Type', true); - $this->set('Status', '415 Unsupported Media Type', true); + $this->set('Status', '415', true); break; case RequestStatusCode::R_416: $this->set('', 'HTTP/1.0 416 Requested Range Not Satisfiable', true); - $this->set('Status', '416 Requested Range Not Satisfiable', true); + $this->set('Status', '416', true); break; case RequestStatusCode::R_417: $this->set('', 'HTTP/1.0 417 Expectation Failed', true); - $this->set('Status', '417 Expectation Failed', true); + $this->set('Status', '417', true); break; case RequestStatusCode::R_421: $this->set('', 'HTTP/1.0 421 Misdirected Request', true); - $this->set('Status', '421 Misdirected Request', true); + $this->set('Status', '421', true); break; case RequestStatusCode::R_422: $this->set('', 'HTTP/1.0 422 Unprocessable Entity', true); - $this->set('Status', '422 Unprocessable Entity', true); + $this->set('Status', '422', true); break; case RequestStatusCode::R_423: $this->set('', 'HTTP/1.0 423 Locked', true); - $this->set('Status', '423 Locked', true); + $this->set('Status', '423', true); break; case RequestStatusCode::R_424: $this->set('', 'HTTP/1.0 424 Failed Dependency', true); - $this->set('Status', '424 Failed Dependency', true); + $this->set('Status', '424', true); break; case RequestStatusCode::R_425: $this->set('', 'HTTP/1.0 425 Too Early', true); - $this->set('Status', '425 Too Early', true); + $this->set('Status', '425', true); break; case RequestStatusCode::R_426: $this->set('', 'HTTP/1.0 426 Upgrade Required', true); - $this->set('Status', '426 Upgrade Required', true); + $this->set('Status', '426', true); break; case RequestStatusCode::R_428: $this->set('', 'HTTP/1.0 428 Precondition Required', true); - $this->set('Status', '428 Precondition Required', true); + $this->set('Status', '428', true); break; case RequestStatusCode::R_429: $this->set('', 'HTTP/1.0 429 Too Many Requests', true); - $this->set('Status', '429 Too Many Requests', true); + $this->set('Status', '429', true); break; case RequestStatusCode::R_431: $this->set('', 'HTTP/1.0 431 Request Header Fields Too Large', true); - $this->set('Status', '431 Request Header Fields Too Large', true); + $this->set('Status', '431', true); break; case RequestStatusCode::R_451: $this->set('', 'HTTP/1.0 451 Unavailable For Legal Reasons', true); - $this->set('Status', '451 Unavailable For Legal Reasons', true); + $this->set('Status', '451', true); break; case RequestStatusCode::R_501: $this->set('', 'HTTP/1.0 501 Not Implemented', true); - $this->set('Status', '501 Not Implemented', true); + $this->set('Status', '501', true); break; case RequestStatusCode::R_502: $this->set('', 'HTTP/1.0 502 Bad Gateway', true); - $this->set('Status', '502 Bad Gateway', true); + $this->set('Status', '502', true); break; case RequestStatusCode::R_503: $this->set('', 'HTTP/1.0 503 Service Temporarily Unavailable', true); - $this->set('Status', '503 Service Temporarily Unavailable', true); + $this->set('Status', '503', true); $this->set('Retry-After', 'Retry-After: 300', true); break; case RequestStatusCode::R_504: $this->set('', 'HTTP/1.0 504 Gateway Timeout', true); - $this->set('Status', '504 Gateway Timeout', true); + $this->set('Status', '504', true); break; case RequestStatusCode::R_505: $this->set('', 'HTTP/1.0 505 HTTP Version Not Supported', true); - $this->set('Status', '505 HTTP Version Not Supported', true); + $this->set('Status', '505', true); break; case RequestStatusCode::R_506: $this->set('', 'HTTP/1.0 506 HTTP Variant Also Negotiates', true); - $this->set('Status', '506 HTTP Variant Also Negotiates', true); + $this->set('Status', '506', true); break; case RequestStatusCode::R_507: $this->set('', 'HTTP/1.0 507 Insufficient Storage', true); - $this->set('Status', '507 Insufficient Storage', true); + $this->set('Status', '507', true); break; case RequestStatusCode::R_508: $this->set('', 'HTTP/1.0 508 Loop Detected', true); - $this->set('Status', '508 Loop Detected', true); + $this->set('Status', '508', true); break; case RequestStatusCode::R_510: $this->set('', 'HTTP/1.0 510 Not Extended', true); - $this->set('Status', '510 Not Extended', true); + $this->set('Status', '510', true); break; case RequestStatusCode::R_511: $this->set('', 'HTTP/1.0 511 Network Authentication Required', true); - $this->set('Status', '511 Network Authentication Required', true); + $this->set('Status', '511', true); break; case RequestStatusCode::R_500: default: $this->set('', 'HTTP/1.0 500 Internal Server Error', true); - $this->set('Status', '500 Internal Server Error', true); + $this->set('Status', '500', true); } } } From eff9f243d9153fcde0aaf27b3c523d27701809b2 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 5 Oct 2023 11:58:36 +0000 Subject: [PATCH 05/11] fixes #299 --- Stdlib/Graph/Node.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Stdlib/Graph/Node.php b/Stdlib/Graph/Node.php index 5a80c64c5..ca0fff92a 100755 --- a/Stdlib/Graph/Node.php +++ b/Stdlib/Graph/Node.php @@ -250,9 +250,13 @@ class Node foreach ($this->edges as $edge) { $nodes = $edge->getNodes(); - $neighbors[] = $nodes[0] !== null && !$this->isEqual($nodes[0]) - ? $nodes[0] - : $nodes[1]; + if ($edge->isDirected && isset($nodes[1]) && !$this->isEqual($nodes[1])) { + $neighbors[] = $nodes[1]; + } else { + $neighbors[] = isset($nodes[0]) && !$this->isEqual($nodes[0]) + ? $nodes[0] + : $nodes[1]; + } } return $neighbors; From 99211861eb60cc2d0baa1f4408d88a08e412f46e Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Mon, 9 Oct 2023 22:06:39 +0000 Subject: [PATCH 06/11] update --- Algorithm/Clustering/Kmeans.php | 2 +- Algorithm/Maze/MazeGenerator.php | 2 +- Business/BusinessHelper.php | 27 ++++++ .../Database/Mapper/DataMapperFactory.php | 14 +++ DataStorage/Database/Mapper/MapperType.php | 6 +- DataStorage/Database/Mapper/ReadMapper.php | 42 ++++++++- DataStorage/Database/Mapper/WriteMapper.php | 7 +- Localization/ISO3166NameEnum.php | 22 ++--- Localization/ISO3166Trait.php | 44 ++++++++- Localization/RegionEnum.php | 92 +++++++++++++++++++ Message/ResponseAbstract.php | 2 +- Stdlib/Base/Enum.php | 3 +- Stdlib/Base/EnumArray.php | 4 +- Stdlib/Base/FloatInt.php | 6 +- Stdlib/Base/SmartDateTime.php | 44 +++++++++ Utils/Barcode/QR.php | 2 +- Utils/Parser/Markdown/Markdown.php | 8 +- Utils/RnG/File.php | 4 +- Utils/RnG/Name.php | 4 +- Utils/RnG/Phone.php | 2 +- tests/Message/Http/HttpResponseTest.php | 2 +- tests/Message/ResponseAbstractTest.php | 4 +- tests/Module/ModuleAbstractTest.php | 4 +- 23 files changed, 302 insertions(+), 45 deletions(-) create mode 100644 Business/BusinessHelper.php create mode 100644 Localization/RegionEnum.php diff --git a/Algorithm/Clustering/Kmeans.php b/Algorithm/Clustering/Kmeans.php index e11bc7e55..99399320e 100755 --- a/Algorithm/Clustering/Kmeans.php +++ b/Algorithm/Clustering/Kmeans.php @@ -207,7 +207,7 @@ final class Kmeans */ private function kpp(array $points, int $n) : array { - $clusters = [clone $points[\mt_rand(0, \count($points) - 1)]]; + $clusters = [clone $points[\array_rand($points, 1)]]; $d = \array_fill(0, $n, 0.0); for ($i = 1; $i < $n; ++$i) { diff --git a/Algorithm/Maze/MazeGenerator.php b/Algorithm/Maze/MazeGenerator.php index 1b8df0e3e..8b180257e 100755 --- a/Algorithm/Maze/MazeGenerator.php +++ b/Algorithm/Maze/MazeGenerator.php @@ -81,7 +81,7 @@ class MazeGenerator if (!empty($neighbors)) { --$n; - $next = $neighbors[\mt_rand(0, \count($neighbors) - 1)]; + $next = $neighbors[\array_rand($neighbors, 1)]; $unvisited[$next[0] + 1][$next[1] + 1] = false; if ($next[0] === $pos[0]) { diff --git a/Business/BusinessHelper.php b/Business/BusinessHelper.php new file mode 100644 index 000000000..b15bffff5 --- /dev/null +++ b/Business/BusinessHelper.php @@ -0,0 +1,27 @@ +count(); } + /** + * Create read mapper + * + * @param ConnectionAbstract $db Database connection + * + * @return ReadMapper + * + * @since 1.0.0 + */ + public static function sum(ConnectionAbstract $db = null) : ReadMapper + { + return (new ReadMapper(new static(), $db ?? self::$db))->sum(); + } + /** * Create read mapper * diff --git a/DataStorage/Database/Mapper/MapperType.php b/DataStorage/Database/Mapper/MapperType.php index c7fa29c8f..158ea6f2c 100755 --- a/DataStorage/Database/Mapper/MapperType.php +++ b/DataStorage/Database/Mapper/MapperType.php @@ -40,9 +40,11 @@ abstract class MapperType extends Enum public const COUNT_MODELS = 12; - public const MODEL_EXISTS = 13; + public const SUM_MODELS = 13; - public const MODEL_HAS_RELATION = 14; + public const MODEL_EXISTS = 14; + + public const MODEL_HAS_RELATION = 15; // -------------------------------------------- // diff --git a/DataStorage/Database/Mapper/ReadMapper.php b/DataStorage/Database/Mapper/ReadMapper.php index 812057805..551d0864d 100755 --- a/DataStorage/Database/Mapper/ReadMapper.php +++ b/DataStorage/Database/Mapper/ReadMapper.php @@ -118,6 +118,20 @@ final class ReadMapper extends DataMapperAbstract return $this; } + /** + * Create sum mapper + * + * @return self + * + * @since 1.0.0 + */ + public function sum() : self + { + $this->type = MapperType::SUM_MODELS; + + return $this; + } + /** * Create exists mapper * @@ -205,6 +219,8 @@ final class ReadMapper extends DataMapperAbstract return $this->executeGetRaw(); case MapperType::COUNT_MODELS: return $this->executeCount(); + case MapperType::SUM_MODELS: + return $this->executeSum(); case MapperType::MODEL_EXISTS: return $this->executeExists(); case MapperType::MODEL_HAS_RELATION: @@ -404,11 +420,35 @@ final class ReadMapper extends DataMapperAbstract */ public function executeCount() : int { - $query = $this->getQuery(null, ['COUNT(*)' => 'count']); + $query = $this->getQuery( + null, + [ + 'COUNT(' . (empty($this->columns) ? '*' : \implode($this->columns)) . ')' => 'count' + ] + ); return (int) $query->execute()?->fetchColumn(); } + /** + * Sum the number of elements + * + * @return int + * + * @since 1.0.0 + */ + public function executeSum() : int|float + { + $query = $this->getQuery( + null, + [ + 'SUM(' . (empty($this->columns) ? '*' : \implode($this->columns)) . ')' => 'sum' + ] + ); + + return $query->execute()?->fetchColumn(); + } + /** * Check if any element exists * diff --git a/DataStorage/Database/Mapper/WriteMapper.php b/DataStorage/Database/Mapper/WriteMapper.php index eacf8ca49..bc7d5f068 100755 --- a/DataStorage/Database/Mapper/WriteMapper.php +++ b/DataStorage/Database/Mapper/WriteMapper.php @@ -232,7 +232,7 @@ final class WriteMapper extends DataMapperAbstract if (isset($this->mapper::BELONGS_TO[$propertyName]['by'])) { // has by (obj is stored as a different model e.g. model = profile but reference/db is account) - if ($this->mapper::BELONGS_TO[$propertyName]['private']) { + if ($this->mapper::BELONGS_TO[$propertyName]['private'] ?? false) { $refClass = new \ReflectionClass($obj); $refProp = $refClass->getProperty($this->mapper::BELONGS_TO[$propertyName]['by']); $obj = $refProp->getValue($obj); @@ -339,9 +339,10 @@ final class WriteMapper extends DataMapperAbstract $relProperty = $relReflectionClass->getProperty($internalName); } - // todo maybe consider to just set the column type to object, and then check for that (might be faster) + // @todo maybe consider to just set the column type to object, and then check for that (might be faster) if (isset($mapper::BELONGS_TO[$internalName]) - || isset($mapper::OWNS_ONE[$internalName])) { + || isset($mapper::OWNS_ONE[$internalName]) + ) { if ($isRelPrivate) { $relProperty->setValue($value, $this->mapper::createNullModel($objId)); } else { diff --git a/Localization/ISO3166NameEnum.php b/Localization/ISO3166NameEnum.php index 3ea40046f..b7f92f05e 100755 --- a/Localization/ISO3166NameEnum.php +++ b/Localization/ISO3166NameEnum.php @@ -78,7 +78,7 @@ class ISO3166NameEnum extends Enum public const _BTN = 'Bhutan'; - public const _BOL = 'Bolivia (Plurinational State of)'; + public const _BOL = 'Bolivia'; public const _BES = 'Bonaire, Sint Eustatius and Saba'; @@ -234,7 +234,7 @@ class ISO3166NameEnum extends Enum public const _IDN = 'Indonesia'; - public const _IRN = 'Iran (Islamic Republic of)'; + public const _IRN = 'Iran'; public const _IRQ = 'Iraq'; @@ -260,9 +260,9 @@ class ISO3166NameEnum extends Enum public const _KIR = 'Kiribati'; - public const _PRK = 'Korea (Democratic People\'s Republic of)'; + public const _PRK = 'North Korea'; - public const _KOR = 'Korea (Republic of)'; + public const _KOR = 'South Korea'; public const _KWT = 'Kuwait'; @@ -314,9 +314,9 @@ class ISO3166NameEnum extends Enum public const _MEX = 'Mexico'; - public const _FSM = 'Micronesia (Federated States of)'; + public const _FSM = 'Micronesia'; - public const _MDA = 'Moldova (Republic of)'; + public const _MDA = 'Moldova'; public const _MCO = 'Monaco'; @@ -364,7 +364,7 @@ class ISO3166NameEnum extends Enum public const _PLW = 'Palau'; - public const _PSE = 'Palestine, State of'; + public const _PSE = 'Palestine'; public const _PAN = 'Panama'; @@ -460,11 +460,11 @@ class ISO3166NameEnum extends Enum public const _SYR = 'Syrian Arab Republic'; - public const _TWN = 'Taiwan, Province of China[a]'; + public const _TWN = 'Taiwan'; public const _TJK = 'Tajikistan'; - public const _TZA = 'Tanzania, United Republic of'; + public const _TZA = 'Tanzania'; public const _THA = 'Thailand'; @@ -494,7 +494,7 @@ class ISO3166NameEnum extends Enum public const _ARE = 'United Arab Emirates'; - public const _GBR = 'United Kingdom of Great Britain and Northern Ireland'; + public const _GBR = 'United Kingdom'; public const _USA = 'United States of America'; @@ -506,7 +506,7 @@ class ISO3166NameEnum extends Enum public const _VUT = 'Vanuatu'; - public const _VEN = 'Venezuela (Bolivarian Republic of)'; + public const _VEN = 'Venezuela'; public const _VNM = 'Viet Nam'; diff --git a/Localization/ISO3166Trait.php b/Localization/ISO3166Trait.php index 0b2451a1c..92c42706a 100644 --- a/Localization/ISO3166Trait.php +++ b/Localization/ISO3166Trait.php @@ -37,10 +37,44 @@ trait ISO3166Trait { /** @var string $code3 */ $code3 = ISO3166TwoEnum::getName($code); + if ($code3 === false) { + $code3 = ''; + } return self::getByName($code3); } + /** + * Get countries in a region + * + * @param string $region Region name + * + * @return array + * + * @since 1.0.0 + */ + public static function getSubregions(string $region) : array + { + $region = \strtolower($region); + + switch ($region) { + case 'continents': + return ['Europe', 'Asia', 'America', 'Oceania', 'Africa']; + case 'europe': + return ['North-Europe', 'South-Europe', 'East-Europe', 'West-Europe']; + case 'asia': + return ['Central-Asia', 'South-Asia', 'Southeast-Asia', 'East-Asia', 'West-Asia']; + case 'america': + return ['North-america', 'South-america', 'Central-america', 'Caribbean']; + case 'oceania': + return ['Australia', 'Polynesia', 'Melanesia', 'Micronesia', 'Antarctica']; + case 'africa': + return ['North-Africa', 'South-Africa', 'East-Africa', 'West-Africa', 'Central-Africa']; + default: + return [$region]; + } + } + /** * Get countries in a region * @@ -55,6 +89,14 @@ trait ISO3166Trait $region = \strtolower($region); switch ($region) { + case 'continents': + return \array_merge( + self::getRegion('europe'), + self::getRegion('asia'), + self::getRegion('america'), + self::getRegion('oceania'), + self::getRegion('africa') + ); case 'europe': return \array_merge( self::getRegion('north-europe'), @@ -83,7 +125,7 @@ trait ISO3166Trait self::getRegion('polynesia'), self::getRegion('melanesia'), self::getRegion('micronesia'), - self::getRegion('antartica') + self::getRegion('antarctica') ); case 'africa': return \array_merge( diff --git a/Localization/RegionEnum.php b/Localization/RegionEnum.php new file mode 100644 index 000000000..86d4eef34 --- /dev/null +++ b/Localization/RegionEnum.php @@ -0,0 +1,92 @@ +data; diff --git a/Stdlib/Base/Enum.php b/Stdlib/Base/Enum.php index 3eb2f59d8..5000cb211 100755 --- a/Stdlib/Base/Enum.php +++ b/Stdlib/Base/Enum.php @@ -70,9 +70,8 @@ abstract class Enum { $reflect = new \ReflectionClass(static::class); $constants = $reflect->getConstants(); - $keys = \array_keys($constants); - return $constants[$keys[\mt_rand(0, \count($constants) - 1)]]; + return $constants[\array_rand($constants, 1)]; } /** diff --git a/Stdlib/Base/EnumArray.php b/Stdlib/Base/EnumArray.php index bc4fb6d93..87a25fb32 100755 --- a/Stdlib/Base/EnumArray.php +++ b/Stdlib/Base/EnumArray.php @@ -119,8 +119,6 @@ abstract class EnumArray */ public static function getRandom() : mixed { - $keys = \array_keys(static::$constants); - - return static::$constants[$keys[\mt_rand(0, \count(static::$constants) - 1)]]; + return static::$constants[\array_rand(static::$constants, 1)]; } } diff --git a/Stdlib/Base/FloatInt.php b/Stdlib/Base/FloatInt.php index e03a0d808..1104f0d18 100755 --- a/Stdlib/Base/FloatInt.php +++ b/Stdlib/Base/FloatInt.php @@ -158,17 +158,15 @@ class FloatInt implements SerializableInterface */ public function getAmount(?int $decimals = 2) : string { - $isNegative = $this->value < 0 ? 1 : 0; - $value = $this->value === 0 ? \str_repeat('0', self::MAX_DECIMALS) : (string) \round($this->value, -self::MAX_DECIMALS + $decimals); - $left = \substr($value, 0, -self::MAX_DECIMALS + $isNegative); + $left = \substr($value, 0, -self::MAX_DECIMALS); /** @var string $left */ $left = $left === false ? '0' : $left; - $right = \substr($value, -self::MAX_DECIMALS + $isNegative); + $right = \substr($value, -self::MAX_DECIMALS); if ($right === false) { throw new \Exception(); // @codeCoverageIgnore diff --git a/Stdlib/Base/SmartDateTime.php b/Stdlib/Base/SmartDateTime.php index e987e75a5..686b347a6 100755 --- a/Stdlib/Base/SmartDateTime.php +++ b/Stdlib/Base/SmartDateTime.php @@ -359,4 +359,48 @@ class SmartDateTime extends \DateTime return $days; } + + public static function startOfYear(int $month = 1) : \DateTime + { + return new \DateTime(\date('Y') . '-' . \sprintf('%02d', $month) . '-01'); + } + + public static function endOfYear(int $month = 1) : \DateTime + { + return new \DateTime(\date('Y') . '-' . self::calculateMonthIndex(13 - $month, $month) . '-31'); + } + + public static function startOfMonth() : \DateTime + { + return new \DateTime(\date('Y-m') . '-01'); + } + + public static function endOfMonth() : \DateTime + { + return new \DateTime(\date('Y-m-t')); + } + + public static function monthDiff(\DateTime $d1, \DateTime $d2) : int + { + $interval = $d1->diff($d2); + + return ($interval->y * 12) + $interval->m; + } + + /** + * Calculates the current month index based on the start of the fiscal year. + * + * @param int $month Current month + * @param int $start Start of the fiscal year (01 = January) + * + * @return int + * + * @since 1.0.0; + */ + public static function calculateMonthIndex(int $month, int $start = 1) : int + { + $mod = ($month - $start); + + return \abs(($mod < 0 ? 12 + $mod : $mod) % 12) + 1; + } } diff --git a/Utils/Barcode/QR.php b/Utils/Barcode/QR.php index b17f38232..6dc3f5d10 100755 --- a/Utils/Barcode/QR.php +++ b/Utils/Barcode/QR.php @@ -1091,7 +1091,7 @@ class QR extends TwoDAbstract $howManuOut = 8 - (self::QR_FIND_FROM_RANDOM % 9); for ($i = 0; $i < $howManuOut; ++$i) { // @note: This is why the same content can result in different QR codes - $remPos = \mt_rand(0, \count($checked_masks) - 1); + $remPos = \array_rand($checked_masks, 1); unset($checked_masks[$remPos]); $checked_masks = \array_values($checked_masks); } diff --git a/Utils/Parser/Markdown/Markdown.php b/Utils/Parser/Markdown/Markdown.php index a7fd75db7..7c1d8bdda 100755 --- a/Utils/Parser/Markdown/Markdown.php +++ b/Utils/Parser/Markdown/Markdown.php @@ -291,7 +291,7 @@ class Markdown if (\strpos($Excerpt['text'], '>') !== false && \preg_match("/^<((mailto:)?{$commonMarkEmail})>/i", $Excerpt['text'], $matches) ){ - $url = $matches[1]; + $url = UriFactory::build($matches[1]); if (!isset($matches[2])) { @@ -496,7 +496,7 @@ class Markdown if (\strpos($Excerpt['context'], 'http') !== false && \preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, \PREG_OFFSET_CAPTURE) ) { - $url = $matches[0][0]; + $url = UriFactory::build($matches[0][0]); return [ 'extent' => \strlen($matches[0][0]), @@ -521,7 +521,7 @@ class Markdown if (\strpos($Excerpt['text'], '>') !== false && \preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { - $url = $matches[1]; + $url = UriFactory::build($matches[1]); return [ 'extent' => \strlen($matches[0]), @@ -3727,7 +3727,7 @@ class Markdown if (\preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { - $Element['attributes']['href'] = $matches[1]; + $Element['attributes']['href'] = UriFactory::build($matches[1]); if (isset($matches[2])) { diff --git a/Utils/RnG/File.php b/Utils/RnG/File.php index dd0938180..22b951659 100755 --- a/Utils/RnG/File.php +++ b/Utils/RnG/File.php @@ -59,8 +59,8 @@ class File $source = self::$extensions; } - $key = \mt_rand(0, \count($source) - 1); + $key = \array_rand($source, 1); - return $source[$key][\mt_rand(0, \count($source[$key]) - 1)]; + return $source[$key][\array_rand($source[$key], 1)]; } } diff --git a/Utils/RnG/Name.php b/Utils/RnG/Name.php index 234932adf..f02a9faec 100755 --- a/Utils/RnG/Name.php +++ b/Utils/RnG/Name.php @@ -493,8 +493,8 @@ class Name */ public static function generateName(array $type, string $origin = 'western') : string { - $rndType = \mt_rand(0, \count($type) - 1); + $rndType = \array_rand($type, 1); - return self::$names[$origin][$type[$rndType]][\mt_rand(0, \count(self::$names[$origin][$type[$rndType]]) - 1)]; + return self::$names[$origin][$type[$rndType]][\array_rand(self::$names[$origin][$type[$rndType]], 1)]; } } diff --git a/Utils/RnG/Phone.php b/Utils/RnG/Phone.php index a9059b8d3..bdfe6f699 100755 --- a/Utils/RnG/Phone.php +++ b/Utils/RnG/Phone.php @@ -52,7 +52,7 @@ class Phone $numberString = \str_replace( '$1', - (string) $countries[\array_keys($countries)[\mt_rand(0, \count($countries) - 1)]], + (string) $countries[\array_rand($countries, 1)], $numberString ); } diff --git a/tests/Message/Http/HttpResponseTest.php b/tests/Message/Http/HttpResponseTest.php index 45f6478d7..571c327ad 100755 --- a/tests/Message/Http/HttpResponseTest.php +++ b/tests/Message/Http/HttpResponseTest.php @@ -58,7 +58,7 @@ final class HttpResponseTest extends \PHPUnit\Framework\TestCase public function testResponseInputOutput() : void { $this->response->setResponse(['a' => 1]); - self::assertEquals(1, $this->response->get('a')); + self::assertEquals(1, $this->response->getData('a')); } /** diff --git a/tests/Message/ResponseAbstractTest.php b/tests/Message/ResponseAbstractTest.php index fd76956a3..f4df8aab2 100755 --- a/tests/Message/ResponseAbstractTest.php +++ b/tests/Message/ResponseAbstractTest.php @@ -53,7 +53,7 @@ final class ResponseAbstractTest extends \PHPUnit\Framework\TestCase */ public function testDefault() : void { - self::assertNull($this->response->get('asdf')); + self::assertNull($this->response->getData('asdf')); self::assertEquals('', $this->response->getBody()); } @@ -75,6 +75,6 @@ final class ResponseAbstractTest extends \PHPUnit\Framework\TestCase public function testDataInputOutput() : void { $this->response->set('asdf', false); - self::assertFalse($this->response->get('asdf')); + self::assertFalse($this->response->getData('asdf')); } } diff --git a/tests/Module/ModuleAbstractTest.php b/tests/Module/ModuleAbstractTest.php index ff14928a7..c586e79bd 100755 --- a/tests/Module/ModuleAbstractTest.php +++ b/tests/Module/ModuleAbstractTest.php @@ -258,7 +258,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase 'message' => 'Test Message!', 'response' => [1, 'test string', 'bool' => true], ], - $response->get('') + $response->getData('') ); } @@ -276,7 +276,7 @@ final class ModuleAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals( [1, 'test string', 'bool' => true], - $response->get('') + $response->getData('') ); } From 567ab3bfcdab7258cd4352edb068a00a4c7b7012 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 12 Oct 2023 22:49:21 +0000 Subject: [PATCH 07/11] todos fixed --- Algorithm/Clustering/Kmeans.php | 28 +- Algorithm/Graph/MarkovChain.php | 154 +++++++- Algorithm/Rating/Glicko2.php | 2 - .../Database/Mapper/DataMapperFactory.php | 5 +- DataStorage/Database/Mapper/ReadMapper.php | 10 + .../Database/Query/Grammar/Grammar.php | 4 - Math/Geometry/ConvexHull/GrahamScan.php | 109 ++++++ Math/Optimization/Simplex.php | 350 ++++++++++-------- Message/Http/HttpResponse.php | 3 +- Message/Http/Rest.php | 6 +- Message/Mail/Email.php | 2 +- Module/ModuleAbstract.php | 3 - Stdlib/Tree/BinarySearchTree.php | 210 +++++++++++ Stdlib/Tree/Node.php | 49 +++ System/MimeType.php | 66 +++- tests/Algorithm/Clustering/KmeansTest.php | 5 + 16 files changed, 816 insertions(+), 190 deletions(-) create mode 100644 Math/Geometry/ConvexHull/GrahamScan.php create mode 100644 Stdlib/Tree/BinarySearchTree.php create mode 100644 Stdlib/Tree/Node.php diff --git a/Algorithm/Clustering/Kmeans.php b/Algorithm/Clustering/Kmeans.php index 99399320e..14e8f05dd 100755 --- a/Algorithm/Clustering/Kmeans.php +++ b/Algorithm/Clustering/Kmeans.php @@ -110,7 +110,7 @@ final class Kmeans * Generate the clusters of the points * * @param PointInterface[] $points Points to cluster - * @param int<0, max> $clusters Amount of clusters + * @param int<1, max> $clusters Amount of clusters * * @return void * @@ -140,8 +140,7 @@ final class Kmeans foreach ($clusterCenters as $center) { for ($i = 0; $i < $coordinates; ++$i) { - // @todo Invalid center coodinate value in like 5 % of the runs - $center->setCoordinate($i, $center->getCoordinate($i) / ($center->group === 0 ? 1 : $center->group)); + $center->setCoordinate($i, $center->getCoordinate($i) / $center->group); } } @@ -149,7 +148,7 @@ final class Kmeans foreach ($points as $point) { $min = $this->nearestClusterCenter($point, $clusterCenters)[0]; - if ($min !== $point->group) { + if ($clusters !== $point->group) { ++$changed; $point->group = $min; } @@ -208,29 +207,40 @@ final class Kmeans private function kpp(array $points, int $n) : array { $clusters = [clone $points[\array_rand($points, 1)]]; - $d = \array_fill(0, $n, 0.0); + + $d = \array_fill(0, $n, 0.0); for ($i = 1; $i < $n; ++$i) { $sum = 0; foreach ($points as $key => $point) { - $d[$key] = $this->nearestClusterCenter($point, \array_slice($clusters, 0, 5))[1]; + $d[$key] = $this->nearestClusterCenter($point, $clusters)[1]; $sum += $d[$key]; } $sum *= \mt_rand(0, \mt_getrandmax()) / \mt_getrandmax(); + $found = false; foreach ($d as $key => $di) { $sum -= $di; - if ($sum <= 0) { - $clusters[$i] = clone $points[$key]; + // The in array check is important to avoid duplicate cluster centers + if ($sum <= 0 && !\in_array($c = $points[$key], $clusters)) { + $clusters[$i] = clone $c; + $found = true; + } + } + + while (!$found) { + if (!\in_array($c = $points[\array_rand($points)], $clusters)) { + $clusters[$i] = clone $c; + $found = true; } } } foreach ($points as $point) { - $point->group = ($this->nearestClusterCenter($point, $clusters)[0]); + $point->group = $this->nearestClusterCenter($point, $clusters)[0]; } return $clusters; diff --git a/Algorithm/Graph/MarkovChain.php b/Algorithm/Graph/MarkovChain.php index 4c986055e..19a3ef10a 100644 --- a/Algorithm/Graph/MarkovChain.php +++ b/Algorithm/Graph/MarkovChain.php @@ -21,9 +21,159 @@ namespace phpOMS\Algorithm\Graph; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 - * - * @todo Implement */ final class MarkovChain { + /** + * Order of the markov chain + * + * @var int + * @since 1.0.0 + */ + private int $order = 1; + + /** + * Trained data + * + * @var array + * @since 1.0.0 + */ + private array $data = []; + + /** + * Constructor + * + * @param int $order Order of the markov chain + * + * @since 1.0.0 + */ + public function __construct(int $order = 1) + { + $this->order = $order; + } + + /** + * Create markov chain based on input + * + * @param array $values Training values + * + * @return void + * + * @since 1.0.0 + */ + public function train(array $values) : void + { + $temp = []; + $length = \count($values) - $this->order; + + $unique = \array_unique($values); + + for ($i = 0; $i < $length; ++$i) { + $key = []; + for ($j = 0; $j < $this->order; ++$j) { + $key[] = $values[$i + $j]; + } + + $keyString = \implode(' ', $key); + + if (!isset($temp[$keyString])) { + foreach ($unique as $value) { + $temp[$keyString][$value] = 0; + } + } + + ++$temp[$keyString][$values[$i + 1]]; + } + + foreach ($temp as $key => $values) { + $sum = \array_sum($values); + foreach ($values as $idx => $value) { + $this->data[$key][$idx] = $value / $sum; + } + } + } + + /** + * Set training data + * + * @param array> $values Training values + * + * @return void + * + * @since 1.0.0 + */ + public function setTraining(array $values) : void + { + $this->data = $values; + } + + public function generate(int $length, array $start = null) : array + { + $orderKeys = \array_keys($this->data); + $orderValues = \array_keys(\reset($this->data)); + + $output = $start ?? \explode(' ', $orderKeys[\array_rand($orderKeys)]); + $key = $output; + + for ($i = $this->order; $i < $length; ++$i) { + $keyString = \implode(' ', $key); + + $prob = \mt_rand(1, 100) / 100; + $cProb = 0.0; + $found = false; + $val = null; + + foreach (($this->data[$keyString] ?? []) as $val => $p) { + $cProb += $p; + + if ($prob <= $cProb) { + $new = $val; + $found = true; + + break; + } + } + + // Couldn't find possible key + if (!$found) { + $new = $orderValues[\array_rand($orderValues)]; + } + + $output[] = $new; + $key[] = $new; + + \array_shift($key); + } + + return $output; + } + + public function pathProbability(array $path) : float + { + $length = \count($path); + if ($length <= $this->order) { + return 0.0; + } + + $key = \array_slice($path, 0, $this->order); + + $prob = 1.0; + for ($i = $this->order; $i < $length; ++$i) { + $prob *= $this->data[\implode($key)][$path[$i]] ?? 0.0; + + $key[] = $path[$i]; + \array_shift($key); + } + + return $prob; + } + + public function stepProbability(array $state, mixed $next) : float + { + if (\count($state) !== $this->order) { + return 0.0; + } + + return $this->data[\implode(' ', $state)][$next] ?? 0.0; + } } diff --git a/Algorithm/Rating/Glicko2.php b/Algorithm/Rating/Glicko2.php index ebdabbcb1..8b19528ff 100644 --- a/Algorithm/Rating/Glicko2.php +++ b/Algorithm/Rating/Glicko2.php @@ -25,8 +25,6 @@ use phpOMS\Math\Solver\Root\Bisection; * @see https://en.wikipedia.org/wiki/Glicko_rating_system * @see http://www.glicko.net/glicko/glicko2.pdf * @since 1.0.0 - * - * @todo: implement */ final class Glicko2 { diff --git a/DataStorage/Database/Mapper/DataMapperFactory.php b/DataStorage/Database/Mapper/DataMapperFactory.php index f18715743..3cf431d76 100755 --- a/DataStorage/Database/Mapper/DataMapperFactory.php +++ b/DataStorage/Database/Mapper/DataMapperFactory.php @@ -695,11 +695,14 @@ class DataMapperFactory } if ($count > $pageLimit) { - if (!$hasNext) { // @todo: can be maybe removed? + // @todo: can be maybe removed? + /* + if (!$hasNext) { \array_pop($data); $hasNext = true; --$count; } + */ if ($count > $pageLimit) { $hasPrevious = true; diff --git a/DataStorage/Database/Mapper/ReadMapper.php b/DataStorage/Database/Mapper/ReadMapper.php index 551d0864d..7d696467d 100755 --- a/DataStorage/Database/Mapper/ReadMapper.php +++ b/DataStorage/Database/Mapper/ReadMapper.php @@ -257,6 +257,7 @@ final class ReadMapper extends DataMapperAbstract // Get remaining objects (not available in memory cache) or remaining where clauses. //$dbData = $this->executeGetRaw($query); + $ids = []; foreach ($this->executeGetRawYield($query) as $row) { if ($row === []) { continue; @@ -266,6 +267,15 @@ final class ReadMapper extends DataMapperAbstract $obj[$value] = $this->mapper::createBaseModel($row); $obj[$value] = $this->populateAbstract($row, $obj[$value]); + + $ids[] = $value; + + // @todo: This is too slow, since it creates a query for every $row x relation type. + // Pulling it out would be nice. + // The problem with solving this is that in a many-to-many relationship a relation table is used + // BUT the relation data is not available in the object itself meaning after retrieving the object + // it cannot get assigned to the correct parent object. + // Other relation types are easy because either the parent or child object contain the relation info. $this->loadHasManyRelations($obj[$value]); } diff --git a/DataStorage/Database/Query/Grammar/Grammar.php b/DataStorage/Database/Query/Grammar/Grammar.php index e349a6066..f60acbfa8 100755 --- a/DataStorage/Database/Query/Grammar/Grammar.php +++ b/DataStorage/Database/Query/Grammar/Grammar.php @@ -28,10 +28,6 @@ use phpOMS\DataStorage\Database\Query\Where; * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 - * - * @todo Karaka/phpOMS#33 - * Implement missing grammar & builder functions - * Missing elements are e.g. sum, merge etc. */ class Grammar extends GrammarAbstract { diff --git a/Math/Geometry/ConvexHull/GrahamScan.php b/Math/Geometry/ConvexHull/GrahamScan.php new file mode 100644 index 000000000..e1a98ec27 --- /dev/null +++ b/Math/Geometry/ConvexHull/GrahamScan.php @@ -0,0 +1,109 @@ + $points Points (Point Cloud) + * + * @return array + * + * @since 1.0.0 + */ + public static function createConvexHull(array $points) : array + { + $count = \count($points); + + if ($count < 3) { + return []; + } + + $min = 1; + $points = \array_merge([null], $points); + + for ($i = 2; $i < $count; ++$i) { + if ($points[$i]['y'] < $points[$min]['y'] || ($points[$i]['y'] == $points[$min]['y'] && $points[$i]['x'] < $points[$min]['x'])) { + $min = $i; + } + } + + $temp = $points[1]; + $points[1] = $points[$min]; + $points[$min] = $temp; + + $c = $points[1]; + + $subpoints = \array_slice($points, 2, $count); + \usort($subpoints, function (array $a, array $b) use ($c) : bool + { + return atan2($a['y'] - $c['y'], $a['x'] - $c['x']) < atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); + } + ); + + $points = \array_merge([$points[0], $points[1]], $subpoints); + $points[0] = $points[$count]; + + $size = 1; + for ($i = 2; $i <= $count; ++$i) { + while (self::ccw($points[$size - 1], $points[$size], $points[$i]) <= 0) { + if ($size > 1) { + --$size; + } elseif ($i === $count) { + break; + } else { + ++$i; + } + } + + $temp = $points[$i]; + $points[$size + 1] = $points[$i]; + $points[$i] = $points[$size + 1]; + ++$size; + } + + $hull = []; + for ($i = 1; $i <= $size; ++$i) { + $hull[] = $points[$i]; + } + + return $hull; + } + + public static function ccw(array $a, array $b, array $c) + { + return (($b['x'] - $a['x']) * ($c['y'] - $a['y']) - ($b['y'] - $a['y']) * ($c['x'] - $a['x'])); + } +} diff --git a/Math/Optimization/Simplex.php b/Math/Optimization/Simplex.php index 008464be9..190f28a48 100644 --- a/Math/Optimization/Simplex.php +++ b/Math/Optimization/Simplex.php @@ -25,200 +25,210 @@ namespace phpOMS\Math\Optimization; */ class Simplex { - private array $function = []; + private int $m = 0; + private int $n = 0; - private string $functionType = ''; + private array $A = []; - private int|float $functionLimit = 0.0; + private array $b = []; - private array $constraints = []; + private array $c = []; - private array $constraintsType = []; + private int $v = 0; - private array $constraintsLimit = []; + private array $Basic = []; - private array $slackForm = []; + private array $Nonbasic = []; - private array $nonbasicSolution = []; - - private array $basicSolution = []; - - /** - * Define the function to optimize - * - * @param array $function Function to optimize - * - * @return void - * - * @since 1.0.0 - */ - public function setFunction(array $function) : void + private function pivot (int $x, int $y) { - } - - /** - * Add function constraint - * - * @param array $function Constraint function - * @param string $type Constraint type - * @param float $limit Constraint - * - * @return void - * - * @since 1.0.0 - */ - public function addConstraint(array $function, string $type, float $limit) : void - { - } - - /** - * Pivot element - * - * @param int $x X-Pivot - * @param int $y Y-Pivot - * - * @return void - * - * @since 1.0.0 - */ - private function pivot(int $x, int $y) : void - { - } - - /** - * Perform simplex iteration - * - * @return void - * - * @since 1.0.0 - */ - private function iterateSimplex() : void - { - } - - /** - * Initialize simplex algorithm - * - * @return bool - * - * @since 1.0.0 - */ - private function initialize() : bool - { - $k = -1; - $minLimit = -1; - - $m = \count($this->constraints); - $n = \count($this->function); - - for ($i = 0; $i < $m; ++$i) { - if ($k === -1 || $this->constraintsLimit[$i] < $minLimit) { - $k = $i; - $minLimit = $this->constraintsLimit[$i]; + for ($j = 0; $j < $this->n; ++$j) { + if ($j !== $y) { + $this->A[$x][$j] /= -$this->A[$x][$y]; } } - if ($this->constraintsLimit[$k] >= 0) { - for ($j = 0; $j < $n; ++$j) { - $this->nonbasicSolution[$j] = $j; + $this->b[$x] /= -$this->A[$x][$y]; + $this->A[$x][$y] = 1.0 / $this->A[$x][$y]; + + for ($i = 0; $i < $this->m; ++$i) { + if ($i !== $x) { + for ($j = 0; $j < $this->n; ++$j) { + if ($j !== $y) { + $this->A[$i][$j] += $this->A[$i][$y] * $this->A[$x][$j]; + } + } + + $this->b[$i] += $this->A[$i][$y] / $this->b[$x]; + $this->A[$i][$y] *= $this->A[$x][$y]; + } + } + + for ($j = 0; $j < $this->n; ++$j) { + if ($j !== $y) { + $this->c[$j] += $this->c[$y] * $this->A[$x][$j]; + } + } + + $this->v += $this->c[$y] * $this->b[$x]; + $this->c[$y] *= $this->A[$x][$y]; + + $temp = $this->Basic[$x]; + $this->Basic[$x] = $this->Nonbasic[$y]; + $this->Nonbasic[$y] = $temp; + } + + private function iterate() : int + { + $ind = -1; + $best = -1; + + for ($j = 0; $j < $this->n; ++$j) { + if ($this->c[$j] > 0) { + if ($best === -1 || $this->Nonbasic[$j] < $ind) { + $ind = $this->Nonbasic[$j]; + $best = $j; + } + } + } + + if ($ind === -1) { + return 1; + } + + $maxConstraint = \INF; + $bestConstraint = -1; + + for ($i = 0; $i < $this->m; ++$i) { + if ($this->A[$i][$best] < 0) { + $currentConstraint = -$this->b[$i] / $this->A[$i][$best]; + if ($currentConstraint < $maxConstraint) { + $maxConstraint = $currentConstraint; + $bestConstraint = $i; + } + } + } + + if ($maxConstraint === \INF) { + return -1; + } + + $this->pivot($bestConstraint, $best); + + return 0; + } + + private function initialize() : int + { + $k = -1; + $minB = -1; + + for ($i = 0; $i < $this->m; ++$i) { + if ($k === -1 || $this->b[$i] < $minB) { + $k = $i; + $minB = $this->b[$i]; + } + } + + if ($this->b[$k] >= 0) { + for ($j = 0; $j < $this->n; ++$j) { + $this->Nonbasic[$j] = $j; } - for ($i = 0; $i < $m; ++$i) { - $this->basicSolution[$i] = $n + $i; + for ($i = 0; $i < $this->m; ++$i) { + $this->Basic[$i] = $this->n + $i; } - return true; + return 0; } - // Auxiliary LP - ++$n; - for ($j = 0; $j < $n; ++$j) { - $this->nonbasicSolution[$j] = $j; + ++$this->n; + for ($j = 0; $j < $this->n; ++$j) { + $this->Nonbasic[$j] = $j; } - for ($i = 0; $i < $m; ++$i) { - $this->basicSolution[$i] = $n + $i; + for ($i = 0; $i < $this->m; ++$i) { + $this->Basic[$i] = $this->n + $i; } - $oldFunction = $this->function; - $oldLimit = $this->functionLimit; - - // Auxiliary function - $this->function[$n - 1] = -1; - $this->functionLimit = 0; - - for ($j = 0; $j < $n - 1; ++$j) { - $this->function[$j] = 0; + $oldC = []; + for ($j = 0; $j < $this->n - 1; ++$j) { + $oldC[$j] = $this->c[$j]; } - // Auxiliary constraints - for ($i = 0; $i < $m; ++$i) { - $this->constraints[$i][$n - 1] = 1; + $oldV = $this->v; + + $this->c[$this->n - 1] = -1; + for ($j = 0; $j < $this->n - 1; ++$j) { + $this->c[$j] = 0; } - $this->pivot($k, $n - 1); + $this->v = 0; - // Solve Auxiliary LP - while ($this->iterateSimplex()); - - if ($this->functionLimit !== 0) { - return false; + for ($i = 0; $i < $this->m; ++$i) { + $this->A[$i][$this->n - 1] = 1; } - $zBasic = -1; - for ($i = 0; $i < $m; ++$i) { - if ($this->basicSolution[$i] === $n - 1) { - $zBasic = $i; + $this->pivot($k, $this->n - 1); + + while (!$this->iterate()); + + if ($this->v !== 0) { + return -1; + } + + $basicZ = -1; + for ($i = 0; $i < $this->m; ++$i) { + if ($this->Basic[$i] === $this->n - 1) { + $basicZ = $i; break; } } - if ($zBasic === -1) { - $this->pivot($zBasic, $n - 1); + if ($basicZ !== -1) { + $this->pivot($basicZ, $this->n - 1); } - $zNonBasic = -1; - for ($j = 0; $j < $n; ++$j) { - if ($this->nonbasicSolution[$j] === $n - 1) { - $zNonBasic = $j; + $nonbasicZ = -1; + for ($j = 0; $j < $this->n; ++$j) { + if ($this->Nonbasic[$j] === $this->n - 1) { + $nonbasicZ = $j; break; } } - for ($i = 0; $i < $m; ++$i) { - $this->constraints[$i][$zNonBasic] = $this->constraints[$i][$n - 1]; + for ($i = 0; $i < $this->m; ++$i) { + $this->A[$i][$nonbasicZ] = $this->A[$i][$this->n - 1]; } - $tmp = $this->nonbasicSolution[$n - 1]; - $this->nonbasicSolution[$n - 1] = $this->nonbasicSolution[$zNonBasic]; - $this->nonbasicSolution[$zNonBasic] = $tmp; + $temp = $this->Nonbasic[$nonbasicZ]; + $this->Nonbasic[$nonbasicZ] = $this->Nonbasic[$this->n - 1]; + $this->Nonbasic[$this->n - 1] = $temp; - --$n; - - for ($j = 0; $j < $n; ++$j) { - if ($this->nonbasicSolution[$j] > $n) { - --$this->nonbasicSolution[$j]; + --$this->n; + for ($j = 0; $j < $this->n; ++$j) { + if ($this->Nonbasic[$j] > $this->n) { + --$this->Nonbasic[$j]; } } - for ($i = 0; $i < $m; ++$i) { - if ($this->basicSolution[$i] > $n) { - --$this->basicSolution[$i]; + for ($i = 0; $i < $this->m; ++$i) { + if ($this->Basic[$i] > $this->n) { + --$this->Basic[$i]; } } - $this->functionLimit = $oldLimit; - for ($j = 0; $j < $n; ++$j) { - $this->function[$j] = 0; + for ($j = 0; $j < $this->n; ++$j) { + $this->c[$j] = 0; } - for ($j = 0; $j < $n; ++$j) { + $this->v = $oldV; + + for ($j = 0; $j < $this->n; ++$j) { $ok = false; - - for ($jj = 0; $jj < $n; ++$jj) { - if ($j === $this->nonbasicSolution[$jj]) { - $this->function[$jj] += $oldFunction[$j]; - + for ($k = 0; $k < $this->n; ++$k) { + if ($j = $this->Nonbasic[$k]) { + $this->c[$k] += $oldC[$j]; $ok = true; break; } @@ -228,32 +238,52 @@ class Simplex continue; } - for ($i = 0; $i < $m; ++$i) { - if ($j = $this->basicSolution[$i]) { - for ($jj = 0; $jj < $n; ++$jj) { - $this->function[$jj] += $oldFunction[$j] * $this->constraints[$i][$jj]; + for ($i = 0; $i < $this->m; ++$i) { + if ($j === $this->Basic[$i]) { + for ($k = 0; $k < $this->n; ++$k) { + $this->c[$k] = $oldC[$j] * $this->A[$i][$k]; } - $this->functionLimit += $oldFunction[$j] * $this->constraintsLimit[$i]; + $this->v += $oldC[$j] * $this->b[$i]; break; } } } - return true; + return 0; } - /** - * Solve the optimization - * - * @return array - * - * @since 1.0.0 - */ - public function solve() : array + public function solve(array $A, array $b, array $c) { - if (!$this->initialize()) { - return []; + $this->A = $A; + $this->b = $b; + $this->c = $c; + + // @todo: createSlackForm() required? + + $this->m = \count($A); + $this->n = \count(\reset($A)); + + if ($this->initialize() === -1) { + return [\array_fill(0, $this->m + $this->n, -2), \INF]; } + + $code = 0; + while (!($code = $this->iterate())); + + if ($code === -1) { + return [\array_fill(0, $this->m + $this->n, -1), \INF]; + } + + $result = []; + for ($j = 0; $j < $this->n; ++$j) { + $result[$this->Nonbasic[$j]] = 0; + } + + for ($i = 0; $i < $this->m; ++$i) { + $result[$this->Basic[$i]] = $this->b[$i]; + } + + return [$result, $this->v]; } } diff --git a/Message/Http/HttpResponse.php b/Message/Http/HttpResponse.php index 68b074a93..6b72923f5 100755 --- a/Message/Http/HttpResponse.php +++ b/Message/Http/HttpResponse.php @@ -125,8 +125,7 @@ final class HttpResponse extends ResponseAbstract implements RenderableInterface } } - /** @var array{0:bool} $data */ - return $this->getRaw($data[0] ?? false); + return $this->getRaw(false); } /** diff --git a/Message/Http/Rest.php b/Message/Http/Rest.php index e7e305ec7..20e96aa53 100755 --- a/Message/Http/Rest.php +++ b/Message/Http/Rest.php @@ -80,7 +80,7 @@ final class Rest \curl_setopt($curl, \CURLOPT_POST, 1); // handle different content types - $contentType = $requestHeaders['content-type'] ?? []; + $contentType = $requestHeaders['Content-Type'] ?? []; if (empty($contentType) || \in_array(MimeType::M_POST, $contentType)) { /* @phpstan-ignore-next-line */ \curl_setopt($curl, \CURLOPT_POSTFIELDS, \http_build_query($request->data)); @@ -94,7 +94,7 @@ final class Rest // @todo: Replace boundary/ with the correct boundary= in the future. // Currently this cannot be done due to a bug. If we do it now the server cannot correclty populate php://input - $headers['content-type'] = 'Content-Type: multipart/form-data; boundary/' . $boundary; + $headers['Content-Type'] = 'Content-Type: multipart/form-data; boundary/' . $boundary; $headers['content-length'] = 'Content-Length: ' . \strlen($data); \curl_setopt($curl, \CURLOPT_HTTPHEADER, $headers); @@ -145,7 +145,7 @@ final class Rest \curl_close($curl); $raw = \substr(\is_bool($result) ? '' : $result, $len === false ? 0 : $len); - if (\stripos(\implode('', $response->header->get('content-type')), MimeType::M_JSON) !== false) { + if (\stripos(\implode('', $response->header->get('Content-Type')), MimeType::M_JSON) !== false) { $temp = \json_decode($raw, true); if (!\is_array($temp)) { $temp = []; diff --git a/Message/Mail/Email.php b/Message/Mail/Email.php index ff7e85c44..5ef5d13f2 100755 --- a/Message/Mail/Email.php +++ b/Message/Mail/Email.php @@ -2277,7 +2277,7 @@ class Email implements MessageInterface 'subject', 'reply-to', 'message-id', - 'content-type', + 'Content-Type', 'mime-version', 'x-mailer', ]; diff --git a/Module/ModuleAbstract.php b/Module/ModuleAbstract.php index c5be60a9a..99b64a168 100755 --- a/Module/ModuleAbstract.php +++ b/Module/ModuleAbstract.php @@ -545,7 +545,6 @@ abstract class ModuleAbstract mixed $obj ) : void { - // @todo: consider to set different status code? (also for other createInvalid() functions) $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true); $response->data[$request->uri->__toString()] = [ 'status' => NotificationLevel::WARNING, @@ -719,8 +718,6 @@ abstract class ModuleAbstract * * @return void * - * @todo find a way to offload this to the cli in a different process (same for other similar functions) - * * @since 1.0.0 */ protected function createModel(int $account, mixed $obj, string | \Closure $mapper, string $trigger, string $ip) : void diff --git a/Stdlib/Tree/BinarySearchTree.php b/Stdlib/Tree/BinarySearchTree.php new file mode 100644 index 000000000..703483185 --- /dev/null +++ b/Stdlib/Tree/BinarySearchTree.php @@ -0,0 +1,210 @@ +root = $root; + } + + public function search(mixed $data) : ?Node + { + if ($this->root === null) { + return null; + } + + $comparison = $this->root->compare($data); + + if ($comparison > 0) { + return $this->root->left->search($data); + } elseif ($comparison < 0) { + return $this->root->right->search($data); + } + + return $this->root; + } + + public function minimum() : ?Node + { + if ($this->root === null) { + return null; + } + + if ($this->root->left === null) { + return $this->root; + } + + return $this->root->left->minimum(); + } + + public function maximum() : ?Node + { + if ($this->root === null) { + return null; + } + + if ($this->root->right === null) { + return $this->root; + } + + return $this->root->right->minimum(); + } + + public function predecessor(Node $node) : ?Node + { + if ($node->left !== null) { + return $node->left->maximum(); + } + + $top = $node->parent; + while ($top !== Null && $top->compare($node->data)) { + $node = $top; + $top = $top->parent; + } + + return $top; + } + + public function successor(Node $node) : ?Node + { + if ($node->right !== null) { + return $node->right->minimum(); + } + + $top = $node->parent; + while ($top !== null && $top->compare($node->data)) { + $node = $top; + $top = $top->parent; + } + + return $top; + } + + public function insert(Node $node) : void + { + if ($this->root === null) { + $new = new Node($node->key, $node->data); + $new->parent = null; + $new->tree = $this; + + $this->root = $new; + + return; + } + + $current = $this->root; + while (true) { + $comparison = $node->compare($current->data); + + if ($comparison < 0) { + if ($current->left === null) { + $BST = new BinarySearchTree(); + $new = new Node($node->key, $node->data); + $new->parent = $current; + $new->tree = $BST; + + $BST->root = $new; + $current->left = $BST; + } else { + $current = $current->left->root; + } + } elseif ($comparison > 0) { + if ($current->right === null) { + $BST = new BinarySearchTree(); + $new = new Node($node->key, $node->data); + $new->parent = $current; + $new->tree = $BST; + + $BST->root = $new; + $current->right = $BST; + } else { + $current = $current->right->root; + } + } + + return; + } + } + + public function delete(Node &$node) : void + { + if ($node->left === null && $node->right === null) { + if ($node->parent !== null) { + if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { + $node->parent->left = null; + } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node) === 0) { + $node->parent->right = null; + } + } + + $node = null; + + return; + } + + $temp = null; + if ($node->left === null) { + $temp = $node->right->root; + if ($node->parent !== null) { + if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { + $node->parent->left = $temp->tree; + } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node->data) === 0) { + $node->parent->right = $temp->tree; + } + } + + $temp->parent = $node->parent; + + $node = null; + + return; + } + + if ($node->right === null) { + $temp = $node->left->root; + if ($node->parent !== null) { + if ($node->parent->left !== null && $node->parent->left->root->compare($node->data) === 0) { + $node->parent->left = $temp->tree; + } elseif ($node->parent->right !== null && $node->parent->right->root->compare($node->data) === 0) { + $node->parent->right = $temp->tree; + } + } + + $temp->parent = $node->parent; + + $node = null; + + return; + } else { + $temp = $this->successor($node); + $node->key = $temp->key; + $node->data = $temp->data; + + $this->delete($temp); + } + } +} \ No newline at end of file diff --git a/Stdlib/Tree/Node.php b/Stdlib/Tree/Node.php new file mode 100644 index 000000000..9d2620d91 --- /dev/null +++ b/Stdlib/Tree/Node.php @@ -0,0 +1,49 @@ +key = $key; + $this->data = $data; + } + + public function compare(mixed $data) : int + { + return $this->data <=> $data; + } +} \ No newline at end of file diff --git a/System/MimeType.php b/System/MimeType.php index 27b9a49e6..54fde282e 100755 --- a/System/MimeType.php +++ b/System/MimeType.php @@ -458,8 +458,6 @@ abstract class MimeType extends Enum public const M_EVY = 'application/x-envoy'; - public const M_EXE = 'application/x-msdownload'; - public const M_EXI = 'application/exi'; public const M_EXT = 'application/vnd.novadigm.ext'; @@ -2010,6 +2008,20 @@ abstract class MimeType extends Enum public const M_123 = 'application/vnd.lotus-1-2-3'; + public const M_PEXE = 'vnd.microsoft.portable-executable'; + + public const M_EXE = 'application/exe'; + + public const M_DEXE = 'application/dos-exe'; + + public const M_XEXE = 'application/x-winexe'; + + public const M_MDEXE = 'application/msdos-windows'; + + public const M_MSP = 'application/x-msdos-program'; + + public const M_XMDEXE = 'application/x-msdownload'; + /** * Get mime from file extension * @@ -2036,7 +2048,6 @@ abstract class MimeType extends Enum * @return null|string * * @since 1.0.0 - * @todo continue implementation */ public static function mimeToExtension(string $mime) : ?string { @@ -2046,6 +2057,10 @@ abstract class MimeType extends Enum case self::M_JPEG: case self::M_JPG: return 'jpg'; + case self::M_PNG: + return 'png'; + case self::M_SVG: + return 'svg'; case self::M_BMP: return 'bmp'; case self::M_GIF: @@ -2053,6 +2068,51 @@ abstract class MimeType extends Enum case self::M_HTML: case self::M_HTM: return 'htm'; + case self::M_DOCX: + return 'docx'; + case self::M_DOC: + return 'doc'; + case self::M_ODT: + return 'odt'; + case self::M_XLSX: + return 'xlsx'; + case self::M_XLA: + case self::M_XLS: + return 'xls'; + case self::M_ODS: + return 'ods'; + case self::M_PPTX: + return 'pptx'; + case self::M_PPT: + return 'ppt'; + case self::M_ODP: + return 'odp'; + case self::M_CSV: + return 'csv'; + case self::M_XML: + return 'xml'; + case self::M_JSON: + return 'json'; + case self::M_ZIP: + return 'zip'; + case self::M_7Z: + return '7z'; + case self::M_RAR: + return 'rar'; + case self::M_TAR: + return 'tar'; + case self::M_MP3: + return 'mp3'; + case self::M_MP4: + return 'mp4'; + case self::M_PEXE: + case self::M_EXE: + case self::M_DEXE: + case self::M_XEXE: + case self::M_MDEXE: + case self::M_MSP: + case self::M_XMDEXE: + return 'exe'; default: return null; } diff --git a/tests/Algorithm/Clustering/KmeansTest.php b/tests/Algorithm/Clustering/KmeansTest.php index 5a2a30a40..861f745ab 100755 --- a/tests/Algorithm/Clustering/KmeansTest.php +++ b/tests/Algorithm/Clustering/KmeansTest.php @@ -17,6 +17,8 @@ namespace phpOMS\tests\Algorithm\Clustering; use phpOMS\Algorithm\Clustering\Kmeans; use phpOMS\Algorithm\Clustering\Point; +include __DIR__ . '/../../Autoloader.php'; + /** * @testdox phpOMS\tests\Algorithm\Clustering\KmeansTest: Clustering points/elements with the K-means algorithm * @@ -34,6 +36,9 @@ final class KmeansTest extends \PHPUnit\Framework\TestCase $seed = \mt_rand(\PHP_INT_MIN, \PHP_INT_MAX); \mt_srand($seed); + // The following seed + putting the loop to 1 would fail the test + //\mt_srand(1788576141); + $result = false; // due to the random nature this can be false sometimes?! From 2655c816a950c8f0ce8c1708fd78fae3cbcdd832 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 15 Oct 2023 11:53:12 +0000 Subject: [PATCH 08/11] fix model visibilities --- Localization/Defaults/City.php | 12 ++++++------ Localization/Defaults/Country.php | 12 ++++++------ Localization/Defaults/Currency.php | 14 +++++++------- Localization/Defaults/Iban.php | 8 ++++---- Localization/Defaults/Language.php | 10 +++++----- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Localization/Defaults/City.php b/Localization/Defaults/City.php index 9112d5b47..953e63425 100755 --- a/Localization/Defaults/City.php +++ b/Localization/Defaults/City.php @@ -38,7 +38,7 @@ class City * @var string * @since 1.0.0 */ - protected string $countryCode = ''; + public string $countryCode = ''; /** * State code. @@ -46,7 +46,7 @@ class City * @var string * @since 1.0.0 */ - protected string $state = ''; + public string $state = ''; /** * City name. @@ -54,7 +54,7 @@ class City * @var string * @since 1.0.0 */ - protected string $name = ''; + public string $name = ''; /** * Postal code. @@ -62,7 +62,7 @@ class City * @var int * @since 1.0.0 */ - protected int $postal = 0; + public int $postal = 0; /** * Latitude. @@ -70,7 +70,7 @@ class City * @var float * @since 1.0.0 */ - protected float $lat = 0.0; + public float $lat = 0.0; /** * Longitude. @@ -78,7 +78,7 @@ class City * @var float * @since 1.0.0 */ - protected float $long = 0.0; + public float $long = 0.0; /** * Get city name diff --git a/Localization/Defaults/Country.php b/Localization/Defaults/Country.php index b364d7047..3a9cc9ef6 100755 --- a/Localization/Defaults/Country.php +++ b/Localization/Defaults/Country.php @@ -38,7 +38,7 @@ class Country * @var string * @since 1.0.0 */ - protected string $name = ''; + public string $name = ''; /** * Country code. @@ -46,7 +46,7 @@ class Country * @var string * @since 1.0.0 */ - protected string $code2 = ''; + public string $code2 = ''; /** * Country code. @@ -54,7 +54,7 @@ class Country * @var string * @since 1.0.0 */ - protected string $code3 = ''; + public string $code3 = ''; /** * Country code. @@ -62,7 +62,7 @@ class Country * @var int * @since 1.0.0 */ - protected int $numeric = 0; + public int $numeric = 0; /** * Country subdevision. @@ -70,7 +70,7 @@ class Country * @var string * @since 1.0.0 */ - protected string $subdevision = ''; + public string $subdevision = ''; /** * Country developed. @@ -78,7 +78,7 @@ class Country * @var bool * @since 1.0.0 */ - protected bool $isDeveloped = false; + public bool $isDeveloped = false; /** * Get id diff --git a/Localization/Defaults/Currency.php b/Localization/Defaults/Currency.php index c7ab0cf5e..1aa16f62c 100755 --- a/Localization/Defaults/Currency.php +++ b/Localization/Defaults/Currency.php @@ -38,7 +38,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $name = ''; + public string $name = ''; /** * Currency code. @@ -46,7 +46,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $code = ''; + public string $code = ''; /** * Currency symbol. @@ -54,7 +54,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $symbol = ''; + public string $symbol = ''; /** * Currency number. @@ -62,7 +62,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $number = ''; + public string $number = ''; /** * Currency subunits. @@ -70,7 +70,7 @@ class Currency * @var int * @since 1.0.0 */ - protected int $subunits = 0; + public int $subunits = 0; /** * Currency decimals. @@ -78,7 +78,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $decimals = ''; + public string $decimals = ''; /** * Currency countries. @@ -86,7 +86,7 @@ class Currency * @var string * @since 1.0.0 */ - protected string $countries = ''; + public string $countries = ''; /** * Get currency name diff --git a/Localization/Defaults/Iban.php b/Localization/Defaults/Iban.php index 06cf41bb5..5ebc3c3de 100755 --- a/Localization/Defaults/Iban.php +++ b/Localization/Defaults/Iban.php @@ -38,7 +38,7 @@ class Iban * @var string * @since 1.0.0 */ - protected string $country = ''; + public string $country = ''; /** * Iban chars. @@ -46,7 +46,7 @@ class Iban * @var int * @since 1.0.0 */ - protected int $chars = 2; + public int $chars = 2; /** * Iban bban. @@ -54,7 +54,7 @@ class Iban * @var string * @since 1.0.0 */ - protected string $bban = ''; + public string $bban = ''; /** * Iban fields. @@ -62,7 +62,7 @@ class Iban * @var string * @since 1.0.0 */ - protected string $fields = ''; + public string $fields = ''; /** * Get iban country diff --git a/Localization/Defaults/Language.php b/Localization/Defaults/Language.php index c2c2360f8..2089e9fbb 100755 --- a/Localization/Defaults/Language.php +++ b/Localization/Defaults/Language.php @@ -38,7 +38,7 @@ class Language * @var string * @since 1.0.0 */ - protected string $name = ''; + public string $name = ''; /** * Language native. @@ -46,7 +46,7 @@ class Language * @var string * @since 1.0.0 */ - protected string $native = ''; + public string $native = ''; /** * Language code. @@ -54,7 +54,7 @@ class Language * @var string * @since 1.0.0 */ - protected string $code2 = ''; + public string $code2 = ''; /** * Language code. @@ -62,7 +62,7 @@ class Language * @var string * @since 1.0.0 */ - protected string $code3 = ''; + public string $code3 = ''; /** * Language code. @@ -70,7 +70,7 @@ class Language * @var string * @since 1.0.0 */ - protected string $code3Native = ''; + public string $code3Native = ''; /** * Get id From 624d6b517ed5460ffe7b2ccc49935303de31dd28 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 15 Oct 2023 12:45:18 +0000 Subject: [PATCH 09/11] code analysis fixes --- Algorithm/Graph/MarkovChain.php | 29 +++++++ Business/BusinessHelper.php | 2 +- DataStorage/Database/GrammarAbstract.php | 2 +- Localization/RegionEnum.php | 2 +- Math/Geometry/ConvexHull/GrahamScan.php | 29 ++++--- Stdlib/Base/SmartDateTime.php | 42 ++++++++++ Stdlib/Tree/BinarySearchTree.php | 100 +++++++++++++++++++---- Stdlib/Tree/Node.php | 57 ++++++++++++- 8 files changed, 234 insertions(+), 29 deletions(-) diff --git a/Algorithm/Graph/MarkovChain.php b/Algorithm/Graph/MarkovChain.php index 19a3ef10a..dde713a45 100644 --- a/Algorithm/Graph/MarkovChain.php +++ b/Algorithm/Graph/MarkovChain.php @@ -107,6 +107,16 @@ final class MarkovChain $this->data = $values; } + /** + * Generate a markov chain based on the training data. + * + * @param int $length Length of the markov chain + * @param array $start Start values of the markov chain + * + * @return array + * + * @since 1.0.0 + */ public function generate(int $length, array $start = null) : array { $orderKeys = \array_keys($this->data); @@ -148,6 +158,15 @@ final class MarkovChain return $output; } + /** + * Calculate the probability for a certain markov chain. + * + * @param array $path Markov chain + * + * @return float + * + * @since 1.0.0 + */ public function pathProbability(array $path) : float { $length = \count($path); @@ -168,6 +187,16 @@ final class MarkovChain return $prob; } + /** + * Calculate the probability for a certain state change in a markov chain + * + * @param array $state Current state of the markov chain + * @param mixed $next Next markov state + * + * @return float + * + * @since 1.0.0 + */ public function stepProbability(array $state, mixed $next) : float { if (\count($state) !== $this->order) { diff --git a/Business/BusinessHelper.php b/Business/BusinessHelper.php index b15bffff5..6b3abff03 100644 --- a/Business/BusinessHelper.php +++ b/Business/BusinessHelper.php @@ -24,4 +24,4 @@ namespace phpOMS\Business; */ final class BusinessHelper { -} \ No newline at end of file +} diff --git a/DataStorage/Database/GrammarAbstract.php b/DataStorage/Database/GrammarAbstract.php index 3bf1d8c43..4ca66d527 100755 --- a/DataStorage/Database/GrammarAbstract.php +++ b/DataStorage/Database/GrammarAbstract.php @@ -204,7 +204,7 @@ abstract class GrammarAbstract $expression .= $element->toSql() . (\is_string($key) ? ' as ' . $key : '') . ', '; } elseif (\is_int($element)) { $expression .= $element . ', '; - }else { + } else { throw new \InvalidArgumentException(); } } diff --git a/Localization/RegionEnum.php b/Localization/RegionEnum.php index 86d4eef34..24b6e4001 100644 --- a/Localization/RegionEnum.php +++ b/Localization/RegionEnum.php @@ -89,4 +89,4 @@ class RegionEnum extends Enum public const ANTARCTICA = 'Antarctica'; public const CONTINENTS = 'Continents'; -} \ No newline at end of file +} diff --git a/Math/Geometry/ConvexHull/GrahamScan.php b/Math/Geometry/ConvexHull/GrahamScan.php index e1a98ec27..2e8aa6afc 100644 --- a/Math/Geometry/ConvexHull/GrahamScan.php +++ b/Math/Geometry/ConvexHull/GrahamScan.php @@ -60,18 +60,16 @@ final class GrahamScan } } - $temp = $points[1]; - $points[1] = $points[$min]; + $temp = $points[1]; + $points[1] = $points[$min]; $points[$min] = $temp; $c = $points[1]; $subpoints = \array_slice($points, 2, $count); - \usort($subpoints, function (array $a, array $b) use ($c) : bool - { - return atan2($a['y'] - $c['y'], $a['x'] - $c['x']) < atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); - } - ); + \usort($subpoints, function (array $a, array $b) use ($c) : bool { + return atan2($a['y'] - $c['y'], $a['x'] - $c['x']) < atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); + }); $points = \array_merge([$points[0], $points[1]], $subpoints); $points[0] = $points[$count]; @@ -88,9 +86,9 @@ final class GrahamScan } } - $temp = $points[$i]; + $temp = $points[$i]; $points[$size + 1] = $points[$i]; - $points[$i] = $points[$size + 1]; + $points[$i] = $points[$size + 1]; ++$size; } @@ -102,7 +100,18 @@ final class GrahamScan return $hull; } - public static function ccw(array $a, array $b, array $c) + /** + * Counterclockwise rotation + * + * @param float[] $a Vector + * @param float[] $b Vector + * @param float[] $c Vector + * + * @return int|float + * + * @since 1.0.0 + */ + public static function ccw(array $a, array $b, array $c) : int|float { return (($b['x'] - $a['x']) * ($c['y'] - $a['y']) - ($b['y'] - $a['y']) * ($c['x'] - $a['x'])); } diff --git a/Stdlib/Base/SmartDateTime.php b/Stdlib/Base/SmartDateTime.php index 686b347a6..30b456207 100755 --- a/Stdlib/Base/SmartDateTime.php +++ b/Stdlib/Base/SmartDateTime.php @@ -360,26 +360,68 @@ class SmartDateTime extends \DateTime return $days; } + /** + * Get the start of the year based on a custom starting month + * + * @param int $month Start of the year (i.e. fiscal year) + * + * @return \DateTime + * + * @since 1.0.0 + */ public static function startOfYear(int $month = 1) : \DateTime { return new \DateTime(\date('Y') . '-' . \sprintf('%02d', $month) . '-01'); } + /** + * Get the end of the year based on a custom starting month + * + * @param int $month Start of the year (i.e. fiscal year) + * + * @return \DateTime + * + * @since 1.0.0 + */ public static function endOfYear(int $month = 1) : \DateTime { return new \DateTime(\date('Y') . '-' . self::calculateMonthIndex(13 - $month, $month) . '-31'); } + /** + * Get the start of the month + * + * @return \DateTime + * + * @since 1.0.0 + */ public static function startOfMonth() : \DateTime { return new \DateTime(\date('Y-m') . '-01'); } + /** + * Get the end of the month + * + * @return \DateTime + * + * @since 1.0.0 + */ public static function endOfMonth() : \DateTime { return new \DateTime(\date('Y-m-t')); } + /** + * Calculate the difference in months between two dates + * + * @param \DateTime $d1 First datetime + * @param \DateTime $d2 Second datetime + * + * @return int + * + * @since 1.0.0 + */ public static function monthDiff(\DateTime $d1, \DateTime $d2) : int { $interval = $d1->diff($d2); diff --git a/Stdlib/Tree/BinarySearchTree.php b/Stdlib/Tree/BinarySearchTree.php index 703483185..0bff1b872 100644 --- a/Stdlib/Tree/BinarySearchTree.php +++ b/Stdlib/Tree/BinarySearchTree.php @@ -24,13 +24,35 @@ namespace phpOMS\Stdlib\Tree; */ class BinarySearchTree { + /** + * Root node + * + * @param null|Node + * @since 1.0.0 + */ public ?Node $root = null; + /** + * Constructor. + * + * @param null|Node $root Root node + * + * @since 1.0.0 + */ public function __construct(Node $root = null) { $this->root = $root; } + /** + * Search node by data + * + * @param mixed $data Data to search for + * + * @return null|Node + * + * @since 1.0.0 + */ public function search(mixed $data) : ?Node { if ($this->root === null) { @@ -48,6 +70,13 @@ class BinarySearchTree return $this->root; } + /** + * Find the smallest node + * + * @return null|Node + * + * @since 1.0.0 + */ public function minimum() : ?Node { if ($this->root === null) { @@ -61,6 +90,13 @@ class BinarySearchTree return $this->root->left->minimum(); } + /** + * Find the largest node + * + * @return null|Node + * + * @since 1.0.0 + */ public function maximum() : ?Node { if ($this->root === null) { @@ -74,6 +110,15 @@ class BinarySearchTree return $this->root->right->minimum(); } + /** + * Find the predecessor of a node + * + * @param Node $node Node + * + * @return null|Node + * + * @since 1.0.0 + */ public function predecessor(Node $node) : ?Node { if ($node->left !== null) { @@ -83,12 +128,21 @@ class BinarySearchTree $top = $node->parent; while ($top !== Null && $top->compare($node->data)) { $node = $top; - $top = $top->parent; + $top = $top->parent; } return $top; } + /** + * Find the successor of a node + * + * @param Node $node Node + * + * @return null|Node + * + * @since 1.0.0 + */ public function successor(Node $node) : ?Node { if ($node->right !== null) { @@ -98,18 +152,27 @@ class BinarySearchTree $top = $node->parent; while ($top !== null && $top->compare($node->data)) { $node = $top; - $top = $top->parent; + $top = $top->parent; } return $top; } + /** + * Insert a node + * + * @param Node $node Node + * + * @return void + * + * @since 1.0.0 + */ public function insert(Node $node) : void { if ($this->root === null) { - $new = new Node($node->key, $node->data); + $new = new Node($node->key, $node->data); $new->parent = null; - $new->tree = $this; + $new->tree = $this; $this->root = $new; @@ -122,24 +185,24 @@ class BinarySearchTree if ($comparison < 0) { if ($current->left === null) { - $BST = new BinarySearchTree(); - $new = new Node($node->key, $node->data); + $BST = new BinarySearchTree(); + $new = new Node($node->key, $node->data); $new->parent = $current; - $new->tree = $BST; + $new->tree = $BST; - $BST->root = $new; + $BST->root = $new; $current->left = $BST; } else { $current = $current->left->root; } } elseif ($comparison > 0) { if ($current->right === null) { - $BST = new BinarySearchTree(); - $new = new Node($node->key, $node->data); + $BST = new BinarySearchTree(); + $new = new Node($node->key, $node->data); $new->parent = $current; - $new->tree = $BST; + $new->tree = $BST; - $BST->root = $new; + $BST->root = $new; $current->right = $BST; } else { $current = $current->right->root; @@ -150,6 +213,15 @@ class BinarySearchTree } } + /** + * Delete a node + * + * @param Node $node Node + * + * @return void + * + * @since 1.0.0 + */ public function delete(Node &$node) : void { if ($node->left === null && $node->right === null) { @@ -201,10 +273,10 @@ class BinarySearchTree return; } else { $temp = $this->successor($node); - $node->key = $temp->key; + $node->key = $temp->key; $node->data = $temp->data; $this->delete($temp); } } -} \ No newline at end of file +} diff --git a/Stdlib/Tree/Node.php b/Stdlib/Tree/Node.php index 9d2620d91..3d8c0ce83 100644 --- a/Stdlib/Tree/Node.php +++ b/Stdlib/Tree/Node.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Stdlib\Tree; /** - * Priority queue class. + * Tree node class. * * @package phpOMS\Stdlib\Tree * @license OMS License 2.0 @@ -24,26 +24,79 @@ namespace phpOMS\Stdlib\Tree; */ class Node { + /** + * Key of the node + * + * @var string + * @since 1.0.0 + */ public string $key = ''; + /** + * Data of the node + * + * @var mixed + * @since 1.0.0 + */ public mixed $data = null; + /** + * Sub-tree to the left + * + * @var null|BinarySearchTree + * @since 1.0.0 + */ public ?BinarySearchTree $left = null; + /** + * Sub-tree to the right + * + * @var null|BinarySearchTree + * @since 1.0.0 + */ public ?BinarySearchTree $right = null; + /** + * Parent node + * + * @var null|Node + * @since 1.0.0 + */ public ?self $parent = null; + /** + * Parent tree + * + * @var null|BinarySearchTree + * @since 1.0.0 + */ public ?BinarySearchTree $tree = null; + /** + * Constructor. + * + * @param string $key Node key + * @param mixed $data Node data + * + * @since 1.0.0 + */ public function __construct(string $key, mixed $data = null) { $this->key = $key; $this->data = $data; } + /** + * Compare node data + * + * @param mixed $data Node data to compare with + * + * @return -1|0|1 + * + * @since 1.0.0 + */ public function compare(mixed $data) : int { return $this->data <=> $data; } -} \ No newline at end of file +} From ae6391ecf899bac5e14bf81d84514e25641b852c Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 15 Oct 2023 13:17:02 +0000 Subject: [PATCH 10/11] fix tests --- tests/Algorithm/Clustering/KmeansTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Algorithm/Clustering/KmeansTest.php b/tests/Algorithm/Clustering/KmeansTest.php index 861f745ab..2da821f82 100755 --- a/tests/Algorithm/Clustering/KmeansTest.php +++ b/tests/Algorithm/Clustering/KmeansTest.php @@ -17,7 +17,7 @@ namespace phpOMS\tests\Algorithm\Clustering; use phpOMS\Algorithm\Clustering\Kmeans; use phpOMS\Algorithm\Clustering\Point; -include __DIR__ . '/../../Autoloader.php'; +include_once __DIR__ . '/../../Autoloader.php'; /** * @testdox phpOMS\tests\Algorithm\Clustering\KmeansTest: Clustering points/elements with the K-means algorithm From 513fa7bf8bf78448dcb42a8c56882f9e52b04e5a Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 15 Oct 2023 13:43:45 +0000 Subject: [PATCH 11/11] test fixes --- Algorithm/Graph/MarkovChain.php | 5 ++--- Math/Geometry/ConvexHull/GrahamScan.php | 13 ++++++++----- Stdlib/Tree/BinarySearchTree.php | 10 +++++----- Stdlib/Tree/Node.php | 2 +- tests/Message/Http/RestTest.php | 8 ++++---- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Algorithm/Graph/MarkovChain.php b/Algorithm/Graph/MarkovChain.php index dde713a45..c74eb26ac 100644 --- a/Algorithm/Graph/MarkovChain.php +++ b/Algorithm/Graph/MarkovChain.php @@ -130,22 +130,21 @@ final class MarkovChain $prob = \mt_rand(1, 100) / 100; $cProb = 0.0; - $found = false; $val = null; + $new = null; foreach (($this->data[$keyString] ?? []) as $val => $p) { $cProb += $p; if ($prob <= $cProb) { $new = $val; - $found = true; break; } } // Couldn't find possible key - if (!$found) { + if ($new === null) { $new = $orderValues[\array_rand($orderValues)]; } diff --git a/Math/Geometry/ConvexHull/GrahamScan.php b/Math/Geometry/ConvexHull/GrahamScan.php index 2e8aa6afc..440d5cc60 100644 --- a/Math/Geometry/ConvexHull/GrahamScan.php +++ b/Math/Geometry/ConvexHull/GrahamScan.php @@ -66,11 +66,14 @@ final class GrahamScan $c = $points[1]; + /** @var array $subpoints */ $subpoints = \array_slice($points, 2, $count); - \usort($subpoints, function (array $a, array $b) use ($c) : bool { - return atan2($a['y'] - $c['y'], $a['x'] - $c['x']) < atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); + \usort($subpoints, function (array $a, array $b) use ($c) : int { + // @todo: Might be wrong order of comparison + return \atan2($a['y'] - $c['y'], $a['x'] - $c['x']) <=> \atan2( $b['y'] - $c['y'], $b['x'] - $c['x']); }); + /** @var array $points */ $points = \array_merge([$points[0], $points[1]], $subpoints); $points[0] = $points[$count]; @@ -103,9 +106,9 @@ final class GrahamScan /** * Counterclockwise rotation * - * @param float[] $a Vector - * @param float[] $b Vector - * @param float[] $c Vector + * @param array $a Vector + * @param array $b Vector + * @param array $c Vector * * @return int|float * diff --git a/Stdlib/Tree/BinarySearchTree.php b/Stdlib/Tree/BinarySearchTree.php index 0bff1b872..7b336f8b8 100644 --- a/Stdlib/Tree/BinarySearchTree.php +++ b/Stdlib/Tree/BinarySearchTree.php @@ -27,7 +27,7 @@ class BinarySearchTree /** * Root node * - * @param null|Node + * @var null|Node * @since 1.0.0 */ public ?Node $root = null; @@ -62,9 +62,9 @@ class BinarySearchTree $comparison = $this->root->compare($data); if ($comparison > 0) { - return $this->root->left->search($data); + return $this->root->left?->search($data); } elseif ($comparison < 0) { - return $this->root->right->search($data); + return $this->root->right?->search($data); } return $this->root; @@ -126,7 +126,7 @@ class BinarySearchTree } $top = $node->parent; - while ($top !== Null && $top->compare($node->data)) { + while ($top !== null && $top->compare($node->data)) { $node = $top; $top = $top->parent; } @@ -272,7 +272,7 @@ class BinarySearchTree return; } else { - $temp = $this->successor($node); + $temp = $this->successor($node); $node->key = $temp->key; $node->data = $temp->data; diff --git a/Stdlib/Tree/Node.php b/Stdlib/Tree/Node.php index 3d8c0ce83..35c5acd03 100644 --- a/Stdlib/Tree/Node.php +++ b/Stdlib/Tree/Node.php @@ -82,7 +82,7 @@ class Node */ public function __construct(string $key, mixed $data = null) { - $this->key = $key; + $this->key = $key; $this->data = $data; } diff --git a/tests/Message/Http/RestTest.php b/tests/Message/Http/RestTest.php index 488e4c34a..f4b7af59b 100755 --- a/tests/Message/Http/RestTest.php +++ b/tests/Message/Http/RestTest.php @@ -54,7 +54,7 @@ final class RestTest extends \PHPUnit\Framework\TestCase $request = new HttpRequest(new HttpUri('https://httpbin.org/post')); $request->setMethod(RequestMethod::POST); self::assertTrue($request->setData('pdata', 'abc')); - self::assertEquals('abc', REST::request($request)->get('form')['pdata'] ?? ''); + self::assertEquals('abc', REST::request($request)->getData('form')['pdata'] ?? ''); } /** @@ -67,7 +67,7 @@ final class RestTest extends \PHPUnit\Framework\TestCase $request = new HttpRequest(new HttpUri('https://httpbin.org/put')); $request->setMethod(RequestMethod::PUT); self::assertTrue($request->setData('pdata', 'abc')); - self::assertEquals('abc', REST::request($request)->get('form')['pdata'] ?? ''); + self::assertEquals('abc', REST::request($request)->getData('form')['pdata'] ?? ''); } /** @@ -80,7 +80,7 @@ final class RestTest extends \PHPUnit\Framework\TestCase $request = new HttpRequest(new HttpUri('https://httpbin.org/delete')); $request->setMethod(RequestMethod::DELETE); self::assertTrue($request->setData('ddata', 'abc')); - self::assertEquals('abc', REST::request($request)->get('form')['ddata'] ?? ''); + self::assertEquals('abc', REST::request($request)->getData('form')['ddata'] ?? ''); } /** @@ -93,7 +93,7 @@ final class RestTest extends \PHPUnit\Framework\TestCase $request = new HttpRequest(new HttpUri('https://httpbin.org/get')); $request->setMethod(RequestMethod::GET); self::assertTrue($request->setData('gdata', 'abc')); - self::assertEquals('abc', REST::request($request)->get('args')['gdata'] ?? ''); + self::assertEquals('abc', REST::request($request)->getData('args')['gdata'] ?? ''); } /**