From 14072474c8d33242638ec88b1aa84002d7fcd743 Mon Sep 17 00:00:00 2001 From: Bas Date: Thu, 27 Jan 2022 13:48:52 +0100 Subject: [PATCH] String function improvements and additions TRIM's second value is optional now. It also accepts a string with characters to remove at the beginnen and the end, instead of just the type integer. Added LTRIM support Added RTRIM support Added FIND_FIRST support Added FIND_LAST support Moved CI QA scripts to separate shell script --- .github/workflows/ci.yml | 24 ++---- bin/qa.sh | 18 +++++ docs/api/functions.md | 40 +++++----- src/AQL/HasStringFunctions.php | 62 ++++++++++++++++ src/Traits/NormalizesStringFunctions.php | 59 +++++++++++++++ tests/Unit/AQL/StringFunctionsTest.php | 95 ++++++++++++++++++++++++ 6 files changed, 260 insertions(+), 38 deletions(-) create mode 100755 bin/qa.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edef9ac..e5e1c46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch, push, pull_request] jobs: run: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: matrix: php-versions: [8.0, 8.1] @@ -25,22 +25,8 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress --no-suggest - - name: PHP Code Sniffer + - name: Run all QA tests if: ${{ always() }} - run: vendor/bin/phpcs - - - name: PHP Mess Detector - if: ${{ always() }} - run: vendor/bin/phpmd src/ text phpmd-ruleset.xml - - - name: PHP Stan - if: ${{ always() }} - run: vendor/bin/phpstan analyse -c phpstan.neon - - - name: Psalm - if: ${{ always() }} - run: vendor/bin/psalm --no-cache - - - name: PHP Unit tests - if: ${{ always() }} - run: vendor/bin/phpunit + run: | + chmod +x "${GITHUB_WORKSPACE}/bin/qa.sh" + "${GITHUB_WORKSPACE}/bin/qa.sh" \ No newline at end of file diff --git a/bin/qa.sh b/bin/qa.sh new file mode 100755 index 0000000..6b6ecbe --- /dev/null +++ b/bin/qa.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +echo "Fix coding style" +./vendor/bin/phpcbf + +echo "Check for remaining coding style errors" +./vendor/bin/phpcs -p --ignore=tests + +echo "Run PHPMD" +./vendor/bin/phpmd src/ text phpmd-ruleset.xml + +echo "Run PHPStan" +./vendor/bin/phpstan analyse -c phpstan.neon + +echo "Run Psalm" +./vendor/bin/psalm --no-cache + +echo "Run PHPUnit" +./vendor/bin/phpunit diff --git a/docs/api/functions.md b/docs/api/functions.md index 78b2d84..1c12327 100644 --- a/docs/api/functions.md +++ b/docs/api/functions.md @@ -135,25 +135,27 @@ The following functions are directly supported in FluentAql. ### String functions -| Description | AQL Function | -|:---------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------| -| concat(...$arguments) | [CONCAT(value1, value2, ... valueN)](https://www.arangodb.com/docs/stable/aql/functions-string.html#concat) | -| concatSeparator($separator, $values) | [CONCAT_SEPARATOR(separator, value1, value2, ... valueN)](https://www.arangodb.com/docs/stable/aql/functions-string.html#concat_separator) | -| levenshteinDistance($value1, $value2) | [LEVENSHTEIN_DISTANCE(value1, value2)](https://www.arangodb.com/docs/stable/aql/functions-string.html#levenshtein_distance) | -| lower($value) | [LOWER(value)](https://www.arangodb.com/docs/stable/aql/functions-string.html#lower) | -| ltrim($value, $char) | [LTRIM(value, char)](https://www.arangodb.com/docs/stable/aql/functions-string.html#ltrim) | -| regexMatches($text, $regex, $caseInsensitive) | [REGEX_MATCHES(text, regex, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_matches) | -| regexReplace($text, $regex, $replacement, $caseInsensitive) | [REGEX_REPLACE(text, search, replacement, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_replace) | -| regexSplit($text, $splitExpression, $caseInsensitive, $limit) | [REGEX_SPLIT(text, splitExpression, caseInsensitive, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_split) | -| regexTest($text, $search, $caseInsensitive) | [REGEX_TEST(text, search, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_test) | -| rtrim($value, $char) | [RTRIM(value, char)](https://www.arangodb.com/docs/stable/aql/functions-string.html#rtrim) | -| split($value, $separator, $limit) | [SPLIT(value, separator, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#split) | -| substitute($text, $search, $replace, $limit) | [SUBSTITUTE(value, search, replace, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#substitute) | -| substring($value, $offset, $length) | [SUBSTRING(value, offset, length)](https://www.arangodb.com/docs/stable/aql/functions-string.html#substitute) | -| tokens($input, $analyzer) | [TOKENS(input, analyzer)](https://www.arangodb.com/docs/stable/aql/functions-string.html#tokens) | -| trim($value, $type) | [TRIM(value, type)](https://www.arangodb.com/docs/stable/aql/functions-string.html#trim) | -| upper($value) | [UPPER(value)](https://www.arangodb.com/docs/stable/aql/functions-string.html#upper) | -| uuid() | [UUID()](https://www.arangodb.com/docs/stable/aql/functions-string.html#uuid) | +| Description | AQL Function | +|:--------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------| +| concat(...$arguments) | [CONCAT(value1, value2, ... valueN)](https://www.arangodb.com/docs/stable/aql/functions-string.html#concat) | +| concatSeparator($separator, $values) | [CONCAT_SEPARATOR(separator, value1, value2, ... valueN)](https://www.arangodb.com/docs/stable/aql/functions-string.html#concat_separator) | +| findFirst($text, $search, $start, $end) | [FIND_FIRST(text, search, start, end)](https://www.arangodb.com/docs/stable/aql/functions-string.html#find_first) | +| findLast($text, $search, $start, $end) | [FIND_Last(text, search, start, end)](https://www.arangodb.com/docs/stable/aql/functions-string.html#find_Last) | +| levenshteinDistance($value1, $value2) | [LEVENSHTEIN_DISTANCE(value1, value2)](https://www.arangodb.com/docs/stable/aql/functions-string.html#levenshtein_distance) | +| lower($value) | [LOWER(value)](https://www.arangodb.com/docs/stable/aql/functions-string.html#lower) | +| ltrim($value, $char) | [LTRIM(value, char)](https://www.arangodb.com/docs/stable/aql/functions-string.html#ltrim) | +| regexMatches($text, $regex, $caseInsensitive) | [REGEX_MATCHES(text, regex, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_matches) | +| regexReplace($text, $regex, $replacement, $caseInsensitive) | [REGEX_REPLACE(text, search, replacement, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_replace) | +| regexSplit($text, $splitExpression, $caseInsensitive, $limit) | [REGEX_SPLIT(text, splitExpression, caseInsensitive, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_split) | +| regexTest($text, $search, $caseInsensitive) | [REGEX_TEST(text, search, caseInsensitive)](https://www.arangodb.com/docs/stable/aql/functions-string.html#regex_test) | +| rtrim($value, $char) | [RTRIM(value, char)](https://www.arangodb.com/docs/stable/aql/functions-string.html#rtrim) | +| split($value, $separator, $limit) | [SPLIT(value, separator, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#split) | +| substitute($text, $search, $replace, $limit) | [SUBSTITUTE(value, search, replace, limit)](https://www.arangodb.com/docs/stable/aql/functions-string.html#substitute) | +| substring($value, $offset, $length) | [SUBSTRING(value, offset, length)](https://www.arangodb.com/docs/stable/aql/functions-string.html#substitute) | +| tokens($input, $analyzer) | [TOKENS(input, analyzer)](https://www.arangodb.com/docs/stable/aql/functions-string.html#tokens) | +| trim($value, $type) | [TRIM(value, type)](https://www.arangodb.com/docs/stable/aql/functions-string.html#trim) | +| upper($value) | [UPPER(value)](https://www.arangodb.com/docs/stable/aql/functions-string.html#upper) | +| uuid() | [UUID()](https://www.arangodb.com/docs/stable/aql/functions-string.html#uuid) | ### Type functions diff --git a/src/AQL/HasStringFunctions.php b/src/AQL/HasStringFunctions.php index f39d46c..1239207 100644 --- a/src/AQL/HasStringFunctions.php +++ b/src/AQL/HasStringFunctions.php @@ -59,6 +59,57 @@ public function contains( return new FunctionExpression('CONTAINS', [$text, $search, $returnIndex]); } + /** + * Return the position of the first occurrence of the string search inside the string text. Positions start at 0. + * + * @link https://www.arangodb.com/docs/stable/aql/functions-string.html#find_first + */ + public function findFirst( + string|object $text, + string|object $search, + int|object $start = null, + int|object $end = null + ): FunctionExpression { + $arguments = [ + 'text' => $text, + 'search' => $search, + ]; + if (isset($start)) { + $arguments['start'] = $start; + } + if (isset($end)) { + $arguments['end'] = $end; + } + + return new FunctionExpression('FIND_FIRST', $arguments); + } + + + /** + * Return the position of the last occurrence of the string search inside the string text. Positions start at 0. + * + * @link https://www.arangodb.com/docs/stable/aql/functions-string.html#find_last + */ + public function findLast( + string|object $text, + string|object $search, + int|object $start = null, + int|object $end = null + ): FunctionExpression { + $arguments = [ + 'text' => $text, + 'search' => $search, + ]; + if (isset($start)) { + $arguments['start'] = $start; + } + if (isset($end)) { + $arguments['end'] = $end; + } + + return new FunctionExpression('FIND_LAST', $arguments); + } + /** * Calculate the Damerau-Levenshtein distance between two strings. * @@ -187,6 +238,17 @@ public function rtrim( return new FunctionExpression('RTRIM', $arguments); } + /** + * Return the soundex fingerprint of value. + * + * @link https://www.arangodb.com/docs/stable/aql/functions-string.html#soundex + */ + public function soundex( + string|object $value, + ): FunctionExpression { + return new FunctionExpression('SOUNDEX', [$value]); + } + /** * Split the given string text into a list of strings, using the separator. * diff --git a/src/Traits/NormalizesStringFunctions.php b/src/Traits/NormalizesStringFunctions.php index 3c8777a..f1e8d07 100644 --- a/src/Traits/NormalizesStringFunctions.php +++ b/src/Traits/NormalizesStringFunctions.php @@ -43,6 +43,60 @@ protected function normalizeContains(QueryBuilder $queryBuilder): void ); } + protected function normalizeFindFirst(QueryBuilder $queryBuilder): void + { + $this->parameters['text'] = $queryBuilder->normalizeArgument( + $this->parameters['text'], + ['Query', 'Reference', 'Bind'] + ); + + $this->parameters['search'] = $queryBuilder->normalizeArgument( + $this->parameters['search'], + ['Query', 'Reference', 'Bind'] + ); + + if (isset($this->parameters['start'])) { + $this->parameters['start'] = $queryBuilder->normalizeArgument( + $this->parameters['start'], + ['Number', 'Query', 'Reference', 'Bind'] + ); + } + + if (isset($this->parameters['end'])) { + $this->parameters['end'] = $queryBuilder->normalizeArgument( + $this->parameters['end'], + ['Number', 'Query', 'Reference', 'Bind'] + ); + } + } + + protected function normalizeFindLast(QueryBuilder $queryBuilder): void + { + $this->parameters['text'] = $queryBuilder->normalizeArgument( + $this->parameters['text'], + ['Query', 'Reference', 'Bind'] + ); + + $this->parameters['search'] = $queryBuilder->normalizeArgument( + $this->parameters['search'], + ['Query', 'Reference', 'Bind'] + ); + + if (isset($this->parameters['start'])) { + $this->parameters['start'] = $queryBuilder->normalizeArgument( + $this->parameters['start'], + ['Number', 'Query', 'Reference', 'Bind'] + ); + } + + if (isset($this->parameters['end'])) { + $this->parameters['end'] = $queryBuilder->normalizeArgument( + $this->parameters['end'], + ['Number', 'Query', 'Reference', 'Bind'] + ); + } + } + protected function normalizeLevenshteinDistance(QueryBuilder $queryBuilder): void { $this->normalizeStrings($queryBuilder); @@ -163,6 +217,11 @@ protected function normalizeRtrim(QueryBuilder $queryBuilder): void ); } + protected function normalizeSoundex(QueryBuilder $queryBuilder): void + { + $this->normalizeStrings($queryBuilder); + } + protected function normalizeSplit(QueryBuilder $queryBuilder): void { $this->parameters['value'] = $queryBuilder->normalizeArgument( diff --git a/tests/Unit/AQL/StringFunctionsTest.php b/tests/Unit/AQL/StringFunctionsTest.php index 2fce834..86afdbc 100644 --- a/tests/Unit/AQL/StringFunctionsTest.php +++ b/tests/Unit/AQL/StringFunctionsTest.php @@ -51,6 +51,90 @@ public function testContains() ); } + public function testContainsReturnsIndex() + { + $qb = new QueryBuilder(); + $qb->return($qb->contains('foobarbaz', 'bar', true)); + self::assertEquals( + 'RETURN CONTAINS(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2, true)', + $qb->get()->query + ); + } + + public function testFindFirst() + { + $qb = new QueryBuilder(); + $qb->return($qb->findFirst('foobarbaz', 'bar')); + self::assertEquals( + 'RETURN FIND_FIRST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2)', + $qb->get()->query + ); + } + + public function testFindFirstWithStart() + { + $qb = new QueryBuilder(); + $qb->return($qb->findFirst('foobarbaz', 'bar', 3)); + self::assertEquals( + 'RETURN FIND_FIRST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2, 3)', + $qb->get()->query + ); + } + + public function testFindFirstWithStartAndEnd() + { + $qb = new QueryBuilder(); + $qb->return($qb->findFirst('foobarbaz', 'bar', 3, 12)); + self::assertEquals( + 'RETURN FIND_FIRST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2, 3, 12)', + $qb->get()->query + ); + } + + public function testFindLast() + { + $qb = new QueryBuilder(); + $qb->return($qb->findLast('foobarbaz', 'bar')); + self::assertEquals( + 'RETURN FIND_LAST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2)', + $qb->get()->query + ); + } + + public function testFindLastWithStart() + { + $qb = new QueryBuilder(); + $qb->return($qb->findLast('foobarbaz', 'bar', 3)); + self::assertEquals( + 'RETURN FIND_LAST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2, 3)', + $qb->get()->query + ); + } + + public function testFindLastWithStartAndEnd() + { + $qb = new QueryBuilder(); + $qb->return($qb->findLast('foobarbaz', 'bar', 3, 12)); + self::assertEquals( + 'RETURN FIND_LAST(@' + . $qb->getQueryId() . '_1, @' + . $qb->getQueryId() . '_2, 3, 12)', + $qb->get()->query + ); + } + public function testContainsReturnIndex() { $qb = new QueryBuilder(); @@ -170,6 +254,17 @@ public function testRightTrimWithChar() ); } + public function testSoundex() + { + $qb = new QueryBuilder(); + $qb->return($qb->soundex('foobarbaz')); + self::assertEquals( + 'RETURN SOUNDEX(@' + . $qb->getQueryId() . '_1)', + $qb->get()->query + ); + } + public function testSplit() { $qb = new QueryBuilder();