diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d3fd591..4856ce6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: oliverearl ko_fi: oliverearl diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index cc8c94c..624a556 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,4 +1,4 @@ -name: dependabot-auto-merge +name: Dependabot Auto-Merge on: pull_request_target permissions: diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml index f30644a..42ce237 100644 --- a/.github/workflows/fix-php-code-style-issues.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -1,4 +1,4 @@ -name: Fix PHP code style issues +name: Linting (Pint) on: push: @@ -19,7 +19,7 @@ jobs: with: ref: ${{ github.head_ref }} - - name: Fix PHP code style issues + - name: Run Laravel Pint and fix styling issues uses: aglipanci/laravel-pint-action@2.6 - name: Commit changes diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 1e5dc43..c83afee 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,4 +1,4 @@ -name: PHPStan +name: Static Analysis (Larastan) on: push: @@ -24,5 +24,5 @@ jobs: - name: Install composer dependencies uses: ramsey/composer-install@v3 - - name: Run PHPStan + - name: Run Larastan run: ./vendor/bin/phpstan --error-format=github diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c22e444..d767248 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,4 @@ -name: run-tests +name: Automated Testing (Pest) on: push: @@ -17,14 +17,12 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.4, 8.3] - laravel: [12.*, 11.*] - stability: [prefer-lowest, prefer-stable] + php: [8.4, 8.5] + laravel: [12.*] + stability: [prefer-stable] include: - laravel: 12.* testbench: 10.* - - laravel: 11.* - testbench: 9.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -54,3 +52,11 @@ jobs: - name: Execute tests run: vendor/bin/pest --ci + + - name: Check Type Coverage + if: matrix.stability == 'prefer-stable' + run: vendor/bin/pest --type-coverage=100 + + - name: Check Profanity + if: matrix.stability == 'prefer-stable' + run: vendor/bin/pest --profanity diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index a6e89a1..3119ba3 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -1,4 +1,4 @@ -name: "Update Changelog" +name: Update Changelog on: release: diff --git a/README.md b/README.md index 331a22a..83fc378 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ large sets of data. ## Requirements -Currently, requires PHP 8.3 or above, and Laravel 11+. +Currently, requires PHP 8.4 or above, and Laravel 12+. ## Installation @@ -79,35 +79,51 @@ $everything = FixedArray::merge( ## Full list of working methods -| Method | Description | Example | -|:---------------|:----------------------------------------------------------------------------|--------------------------------------------------------------:| -| add | Alias for push. | `FixedArray::add('bacon', $arr)` | -| addFrom | Add an array or collection of items to an array. | `FixedArray::addFrom([1, 2, 3], $arr)` | -| contains | Checks whether an item exists in a given array. | `FixedArray::contains('needle', $haystack)` | -| count | Returns the number of items in a given array. | `FixedArray::count($array)` | -| create | Creates a new fixed array. | `FixedArray::create(10)` | -| each | Apply a callback to each item in the array without modifying the original. | `FixedArray::each($array, fn () => var_dump($value))` | -| filter | Applies a filter to a given fixed array. | `FixedArray::filter($array, fn ($value) => $value % 2 === 0)` | -| first | Returns the first element of the array. | `FixedArray::first($array)` | -| fromArray | Creates a new fixed array from a standard array. | `FixedArray::fromArray([1, 2, 3])` | -| fromCollection | Creates a new fixed array from an Illuminate collection. | `FixedArray::fromCollection(collect([1, 2, 3])` | -| getSize | Alias for count. | `FixedArray::getSize($array)` | -| isFixedArray | Returns whether a given item is a fixed array. | `FixedArray::isFixedArray($potentialArray)` | -| last | Returns the last element in an array. | `FixedArray::last($array)` | -| map | Applies a callback to each item in the array and returns it. | `FixedArray::map($array, fn ($value) => (string) $value)` | -| merge | Merges given arrays, fixed arrays, or collections into a given fixed array. | `FixedArray::merge($array, $array2, $array3)` | -| nullify | Overwrite all elements in an array with `null`. | `FixedArray::nullify($array)` | -| offsetExists | Returns whether a given array offset exists. | `FixedArray::offsetExists(3, $haystack)` | -| offsetGet | Retrieves the value at a given array offset. | `FixedArray::offsetGet(3, $haystack)` | -| offsetNull | Replaces the value at a given array offset with `null`. | `FixedArray::offsetNull(3, $haystack)` | -| offsetSet | Replaces the value at a given array offset with a provided value. | `FixedArray::offsetSet(3, $value, $haystack)` | -| pop | Pops the latest value from the array. | `FixedArray::pop($array)` | -| push | Pushes a given value to the first available space on the array. | `FixedArray::push($value, $array)` | -| resize | Alias for setSize. | `FixedArray::resize(10, $array)` | -| second | Returns the second value from the array. | `FixedArray::second($array)` | -| setSize | Resizes the array to a given size. | `FixedArray::setSize(10, $array)` | -| toArray | Converts a fixed array into a standard array. | `FixedArray::toArray($array)` | -| toCollection | Converts a fixed array into an Illuminate collection. | `FixedArray::toCollection($array)` | +| Method | Description | +|:---------------|:--------------------------------------------------------------------| +| add | Alias for push. | +| addFrom | Add an array or collection of items to a fixed array. | +| chunk | Split a fixed array into chunks of a given size. | +| chunkWhile | Split a fixed array into chunks while a callback returns true. | +| contains | Check whether an item exists in a fixed array. | +| create | Create a new fixed array of a given size. | +| dsfargeg | DSFARGEG | +| each | Apply a callback to each item without modifying the original array. | +| fill | Fill the array with a single value. | +| filter | Filter the array using a callback. | +| find | Return the first element matching a callback. | +| findKey | Return the key of the first element matching a callback. | +| findIndex | Alias for findKey. | +| first | Return the first element of the array. | +| fluent | Creates a new fluent interface for chaining methods. | +| flatten | Flatten nested arrays, collections, and fixed arrays. | +| fromArray | Create a fixed array from a standard array. | +| fromCollection | Create a fixed array from an Illuminate collection. | +| getSize | Return the number of elements in the array. | +| isFixedArray | Check whether a value is a fixed array. | +| last | Return the last element of the array. | +| map | Apply a callback to each item and return a new array. | +| merge | Merge multiple arrays, fixed arrays, or collections. | +| nullify | Replace all elements with null. | +| offsetExists | Check if an index exists in the array. | +| offsetGet | Get the value at a specific index. | +| offsetNull | Set a specific index to null. | +| offsetSet | Set a value at a specific index. | +| pop | Remove and return the last element. | +| push | Add a value to the first available space. | +| random | Return a random element from the array. | +| resize | Alias for setSize. | +| reverse | Reverse the order of elements. | +| second | Return the second element. | +| setSize | Resize the array to a given size. | +| shift | Remove and return the first element. | +| shuffle | Shuffle the array in a secure manner. | +| slice | Return a subset of the array. | +| sort | Sort the array optionally using a callback. | +| toArray | Convert the fixed array into a standard PHP array. | +| toCollection | Convert the fixed array into an Illuminate collection. | +| unique | Remove duplicate values from the array. | +| unshift | Prepend one or more values to the array. | ## Testing diff --git a/composer.json b/composer.json index 4e0436b..cd0ef17 100644 --- a/composer.json +++ b/composer.json @@ -17,22 +17,25 @@ } ], "require": { - "php": "^8.3", - "spatie/laravel-package-tools": "^1.16", - "illuminate/contracts": "^11.0|^12.0" + "php": "^8.4", + "spatie/laravel-package-tools": "^1.92", + "illuminate/contracts": "^12.0" }, "require-dev": { - "laravel/pint": "^1.14", - "nunomaduro/collision": "^8.1.1", - "larastan/larastan": "^2.9|^3.0", + "larastan/larastan": "^3.7", + "laravel/pint": "^1.25", + "nunomaduro/collision": "^8.8.2", "orchestra/testbench": "^10.1.0", - "pestphp/pest": "^3.0", - "pestphp/pest-plugin-arch": "^3.0", - "pestphp/pest-plugin-laravel": "^3.0", + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-laravel": "^4.0", + "pestphp/pest-plugin-profanity": "^4.1", + "pestphp/pest-plugin-type-coverage": "*", "phpstan/extension-installer": "^1.3", - "phpstan/phpstan-deprecation-rules": "^1.1|^2.0", - "phpstan/phpstan-phpunit": "^1.3|^2.0", - "spatie/laravel-ray": "^1.35" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "roave/security-advisories": "dev-latest" }, "autoload": { "psr-4": { @@ -51,9 +54,11 @@ "scripts": { "post-autoload-dump": "@composer run prepare", "prepare": "@php vendor/bin/testbench package:discover --ansi", - "analyse": "vendor/bin/phpstan analyse", + "analyse": "vendor/bin/phpstan analyse --memory-limit=2G", "test": "vendor/bin/pest", - "test-coverage": "vendor/bin/pest --coverage", + "test:coverage": "vendor/bin/pest --coverage --min=100 --memory-limit=2G", + "test:type-coverage": "vendor/bin/pest --type-coverage", + "test:profanity": "vendor/bin/pest --profanity", "format": "vendor/bin/pint" }, "config": { @@ -66,7 +71,7 @@ "extra": { "laravel": { "providers": [ - "Petrobolos\\FixedArray\\FixedArrayFunctionsServiceProvider" + "Petrobolos\\FixedArray\\Providers\\FixedArrayServiceProvider" ], "aliases": { "FixedArray": "Petrobolos\\FixedArrayFunctions\\Facades\\FixedArray" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d8fd46c..1ed9871 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 5 + level: 9 paths: - src tmpDir: build/phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cf55f3f..444265e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,23 +1,31 @@ - - - - tests - - - - - - - - - - - - - - - ./src - - + + + + tests + + + + + + + + ./src + + diff --git a/src/Facades/FixedArray.php b/src/Facades/FixedArray.php index c1e6e85..b093e92 100644 --- a/src/Facades/FixedArray.php +++ b/src/Facades/FixedArray.php @@ -7,11 +7,14 @@ use Illuminate\Support\Facades\Facade; use Petrobolos\FixedArray\FixedArray as BaseFixedArray; -/** - * @see FixedArray - */ +/** @mixin \Petrobolos\FixedArray\FixedArray */ class FixedArray extends Facade { + /** + * Get the registered name of the component. + * + * @return class-string + */ protected static function getFacadeAccessor(): string { return BaseFixedArray::class; diff --git a/src/FixedArray.php b/src/FixedArray.php index 6c214c3..05d01dd 100755 --- a/src/FixedArray.php +++ b/src/FixedArray.php @@ -1,9 +1,14 @@ $fixedArray + * + * @see \Petrobolos\FixedArray\FixedArray::push() + * + * @return \SplFixedArray */ - public static function add(mixed $value, SplFixedArray $fixedArray): SplFixedArray + public static function add(SplFixedArray $fixedArray, mixed $value): SplFixedArray { - return self::push($value, $fixedArray); + return self::push($fixedArray, $value); } /** * Adds values from a given array or array-like object into the current fixed array. * - * @param \ArrayAccess|array $items - * @param \SplFixedArray $array - * @return \SplFixedArray + * @param iterable $items + * @param \SplFixedArray $array + * + * @return \SplFixedArray */ - public static function addFrom(ArrayAccess|array $items, SplFixedArray $array): SplFixedArray + public static function addFrom(SplFixedArray $array, iterable $items): SplFixedArray { foreach ($items as $value) { - self::add($value, $array); + self::add($array, $value); } return $array; } + /** + * Split a fixed array into chunks of a given size. + * + * @param \SplFixedArray $array + * + * @throws \InvalidArgumentException + * + * @return \SplFixedArray + */ + public static function chunk(SplFixedArray $array, int $size): SplFixedArray + { + if ($size <= 0) { + throw new InvalidArgumentException('Chunk size must be greater than zero.'); + } + + $chunks = array_chunk(self::toArray($array), $size); + $fixedChunks = array_map(static fn(array $chunk): SplFixedArray => self::fromArray($chunk), $chunks); + + return self::fromArray($fixedChunks, false); + } + + /** + * Chunk a fixed array while the given condition is true. + * + * @param \SplFixedArray $array + * @param callable(mixed $value, mixed $key, ?mixed $previous): bool $callback + * + * @return \SplFixedArray + */ + public static function chunkWhile(SplFixedArray $array, callable $callback): SplFixedArray + { + $chunks = []; + $currentChunk = []; + + $previous = null; + + foreach ($array as $key => $value) { + if ($currentChunk === []) { + $currentChunk[] = $value; + } elseif ($callback($value, $key, $previous)) { + $currentChunk[] = $value; + } else { + $chunks[] = self::fromArray($currentChunk); + $currentChunk = [$value]; + } + + $previous = $value; + } + + if ($currentChunk !== []) { + $chunks[] = self::fromArray($currentChunk); + } + + return self::fromArray($chunks, false); + } + /** * Returns whether a given item is contained within the array. + * + * @param \SplFixedArray $array */ - public static function contains(mixed $item, SplFixedArray $array, bool $useStrict = true): bool + public static function contains(SplFixedArray $array, mixed $item, bool $useStrict = true): bool { + /** @phpstan-ignore-next-line The third parameter can be Boolean, not just true. */ return in_array($item, self::toArray($array), $useStrict); } /** * Returns the size of the array. + * + * @param \SplFixedArray $array */ public static function count(SplFixedArray $array): int { @@ -54,14 +123,41 @@ public static function count(SplFixedArray $array): int /** * Create a new fixed array. + * + * @throws \ValueError + * + * @return \SplFixedArray */ public static function create(int $size = 5): SplFixedArray { return new SplFixedArray($size); } + /** + * User was banned for this post. + * + * @return \SplFixedArray + */ + public static function dsfargeg(): SplFixedArray + { + return self::fromArray(str_split('DSFARGEG')); + } + + /** + * Create a new fluent interface. + */ + public static function fluent(mixed $value, ?int $count = null, bool $preserveKeys = true): FixedArrayable + { + return FixedArrayable::make($value, $count, $preserveKeys); + } + /** * Apply a callback to each item in the array without modifying the original array. + * + * @param \SplFixedArray $array + * @param callable(mixed $value, int $key): void $callback + * + * @return \SplFixedArray */ public static function each(SplFixedArray $array, callable $callback): SplFixedArray { @@ -72,26 +168,131 @@ public static function each(SplFixedArray $array, callable $callback): SplFixedA return $array; } + /** + * Fill a fixed array with the given value. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray + */ + public static function fill(SplFixedArray $array, mixed $value): SplFixedArray + { + for ($i = 0, $max = self::count($array); $i < $max; $i++) { + self::offsetSet($array, $i, $value); + } + + return $array; + } + /** * Apply a filter to a given fixed array. + * + * @param \SplFixedArray $array + * @param callable(mixed $value): bool $callback + * + * @return \SplFixedArray */ public static function filter(SplFixedArray $array, callable $callback): SplFixedArray { $result = array_filter(self::toArray($array), $callback); - return self::fromArray($result); + // Reindex to avoid null-filled gaps caused by preserved keys. + return self::fromArray(array_values($result), false); + } + + /** + * Find the first element in the fixed array that satisfies the given callback. + * + * @param \SplFixedArray $array + * @param callable(mixed $value, int $key): bool $callback + */ + public static function find(SplFixedArray $array, callable $callback): mixed + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $value; + } + } + + return null; + } + + /** + * Find the key (index) of the first element in the fixed array that satisfies the given callback. + * + * @param \SplFixedArray $array + * @param callable(mixed $value, int $key): bool $callback + */ + public static function findKey(SplFixedArray $array, callable $callback): ?int + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $key; + } + } + + return null; + } + + /** + * Alias for findKey. + * + * @see \Petrobolos\FixedArray\FixedArray::findKey() + * + * @param \SplFixedArray $array + * @param callable(mixed $value, int $key): bool $callback + */ + public static function findIndex(SplFixedArray $array, callable $callback): ?int + { + return self::findKey($array, $callback); } /** * Returns the first value from a fixed array. + * + * @throws \RuntimeException + * + * @param \SplFixedArray $array */ public static function first(SplFixedArray $array): mixed { return self::offsetGet(0, $array); } + /** + * Flatten a nested fixed array or iterable into a single-level fixed array. + * + * @param \SplFixedArray $array + * @param int|null $depth The maximum depth to flatten. Null flattens all levels. + * + * @return \SplFixedArray + */ + public static function flatten(SplFixedArray $array, ?int $depth = null): SplFixedArray + { + $result = []; + + $flattenRecursive = static function (iterable $items, ?int $level) use (&$result, &$flattenRecursive): void { + foreach ($items as $item) { + if (($item instanceof SplFixedArray || is_iterable($item)) && ($level === null || $level > 0)) { + $flattenRecursive($item, $level === null ? null : $level - 1); + } else { + $result[] = $item; + } + } + }; + + $flattenRecursive($array, $depth); + + return self::fromArray($result, false); + } + + /** * Import a PHP array into a fixed array. + * + * @param array $array + * + * @return \SplFixedArray */ public static function fromArray(array $array, bool $preserveKeys = true): SplFixedArray { @@ -100,6 +301,10 @@ public static function fromArray(array $array, bool $preserveKeys = true): SplFi /** * Import a collection into a fixed array. + * + * @param \Illuminate\Support\Collection $collection + * + * @return \SplFixedArray */ public static function fromCollection(Collection $collection, bool $preserveKeys = true): SplFixedArray { @@ -108,6 +313,8 @@ public static function fromCollection(Collection $collection, bool $preserveKeys /** * Gets the size of the array. + * + * @param \SplFixedArray $array */ public static function getSize(SplFixedArray $array): int { @@ -124,6 +331,10 @@ public static function isFixedArray(mixed $array): bool /** * Retrieves the last item from the array. + * + * @throws \RuntimeException + * + * @param \SplFixedArray $array */ public static function last(SplFixedArray $array): mixed { @@ -131,41 +342,63 @@ public static function last(SplFixedArray $array): mixed } /** - * Apply a callback to each item in the array and return the new array. + * Apply a callback to each item in the array and return a new fixed array. + * + * @param \SplFixedArray $array + * + * @param callable(mixed): mixed $callback + * + * @return \SplFixedArray */ - public static function map(SplFixedArray $array, callable|string $callback): SplFixedArray + public static function map(SplFixedArray $array, callable $callback): SplFixedArray { - $array = array_map($callback, self::toArray($array)); + $result = array_map($callback, self::toArray($array)); - return self::fromArray($array); + return self::fromArray($result); } + /** - * Merges multiple fixed arrays, arrays, or collections into a single fixed array. + * Merge multiple fixed arrays, arrays, or collections into one fixed array. + * + * @template T + * + * @param \SplFixedArray $target + * @param (\SplFixedArray|iterable) ...$sources + * + * @return \SplFixedArray */ - public static function merge(SplFixedArray $array, SplFixedArray|array|Collection ...$arrays): SplFixedArray + public static function merge(SplFixedArray $target, SplFixedArray|iterable ...$sources): SplFixedArray { - foreach ($arrays as $items) { - foreach ($items as $item) { - self::push($item, $array); + foreach ($sources as $source) { + foreach ($source as $item) { + self::push($target, $item); } } - return $array; + return $target; } /** * Replaces the contents of a fixed array with nulls. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray */ - public static function nullify(SplFixedArray $array): void + public static function nullify(SplFixedArray $array): SplFixedArray { for ($i = 0, $iMax = self::count($array); $i < $iMax; $i++) { self::offsetNull($i, $array); } + + return $array; } /** * Return whether the specified index exists. + * + * @param \SplFixedArray $array */ public static function offsetExists(int $index, SplFixedArray $array): bool { @@ -174,6 +407,10 @@ public static function offsetExists(int $index, SplFixedArray $array): bool /** * Returns the value at the specified index. + * + * @throws \RuntimeException + * + * @param \SplFixedArray $array */ public static function offsetGet(int $index, SplFixedArray $array): mixed { @@ -182,22 +419,32 @@ public static function offsetGet(int $index, SplFixedArray $array): mixed /** * Set a given offset to a null value. + * + * @throws \RuntimeException + * + * @param \SplFixedArray $array */ public static function offsetNull(int $index, SplFixedArray $array): void { - self::offsetSet($index, null, $array); + self::offsetSet($array, $index, null); } /** * Sets a new value at a specified index. + * + * @throws \RuntimeException + * + * @param \SplFixedArray $array */ - public static function offsetSet(int $index, mixed $value, SplFixedArray $array): void + public static function offsetSet(SplFixedArray $array, int $index, mixed $value): void { $array->offsetSet($index, $value); } /** * Pops the latest value from the array. + * + * @param \SplFixedArray $array */ public static function pop(SplFixedArray $array): mixed { @@ -208,7 +455,7 @@ public static function pop(SplFixedArray $array): mixed } $item = self::offsetGet($count - 1, $array); - self::offsetSet($count - 1, null, $array); + self::offsetSet($array, $count - 1, null); return $item; } @@ -216,33 +463,74 @@ public static function pop(SplFixedArray $array): mixed /** * Pushes a given value to the first available space on the array. * If the array is too small, the array size is extended by a single value. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray */ - public static function push(mixed $value, SplFixedArray $array): SplFixedArray + public static function push(SplFixedArray $array, mixed $value): SplFixedArray { - foreach ($array as $index => $item) { - if ($item === null) { - $array[$index] = $value; + $size = self::count($array); - return $array; - } + self::setSize($size + 1, $array); + self::offsetSet($array, $size, $value); + + return $array; + } + + /** + * Returns a random element from the fixed array, or null if empty. + * + * @param \SplFixedArray $array + * + * @throws \Random\RandomException + */ + public static function random(SplFixedArray $array): mixed + { + $count = self::count($array); + + if ($count === 0) { + return null; } - self::setSize((self::count($array) + 1), $array); - self::offsetSet(self::count($array) - 1, $value, $array); + $randomIndex = random_int(0, $count - 1); - return $array; + return self::offsetGet($randomIndex, $array); } /** * Alias for setSize. + * + * @see \Petrobolos\FixedArray\FixedArray::setSize() + * + * @throws \ValueError + * + * @param \SplFixedArray $array */ public static function resize(int $size, SplFixedArray $array): bool { return self::setSize($size, $array); } + /** + * Returns a new SplFixedArray with items in reverse order. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray + */ + public static function reverse(SplFixedArray $array): SplFixedArray + { + return self::fromArray( + array_reverse(self::toArray($array)), + preserveKeys: false, + ); + } + /** * Returns the second value from a fixed array. + * + * @param \SplFixedArray $array */ public static function second(SplFixedArray $array): mixed { @@ -255,14 +543,104 @@ public static function second(SplFixedArray $array): mixed /** * Change the size of an array. + * + * @param \SplFixedArray $array */ public static function setSize(int $size, SplFixedArray $array): bool { return $array->setSize($size); } + /** + * Removes and returns the first item from the array. + * + * @param \SplFixedArray $array + */ + public static function shift(SplFixedArray $array): mixed + { + $count = self::count($array); + + if ($count === 0) { + return null; + } + + $item = self::offsetGet(0, $array); + + // Shift all items to the left and nullify the last slot. + for ($i = 1; $i < $count; $i++) { + self::offsetSet($array, $i - 1, self::offsetGet($i, $array)); + } + + self::offsetNull($count - 1, $array); + + return $item; + } + + /** + * Shuffle the fixed array in place using cryptographically secure randomness. + * + * @param \SplFixedArray $array + * + * @throws \Random\RandomException + * + * @return \SplFixedArray + */ + public static function shuffle(SplFixedArray $array): SplFixedArray + { + $values = self::toArray($array); + $count = count($values); + + for ($i = $count - 1; $i > 0; $i--) { + $j = random_int(0, $i); + + [$values[$i], $values[$j]] = [$values[$j], $values[$i]]; + } + + return self::fromArray($values, false); + } + + /** + * Returns a portion of the array as a new SplFixedArray. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray + */ + public static function slice(SplFixedArray $array, int $offset, ?int $length = null): SplFixedArray + { + return self::fromArray( + array_slice(self::toArray($array), $offset, $length), + preserveKeys: false, + ); + } + + /** + * Sort the fixed array in ascending order. + * + * @param \SplFixedArray $array + * @param callable|null $callback Optional custom sort callback. + * + * @return \SplFixedArray + */ + public static function sort(SplFixedArray $array, ?callable $callback = null): SplFixedArray + { + $values = self::toArray($array); + + if ($callback !== null) { + usort($values, $callback); + } else { + sort($values); + } + + return self::fromArray($values, false); + } + /** * Returns a PHP array from the fixed array. + * + * @param \SplFixedArray $array + * + * @return array */ public static function toArray(SplFixedArray $array): array { @@ -271,9 +649,75 @@ public static function toArray(SplFixedArray $array): array /** * Returns a collection from the fixed array. + * + * @param \SplFixedArray $array + * + * @return \Illuminate\Support\Collection */ public static function toCollection(SplFixedArray $array): Collection { return collect($array); } + + /** + * Returns a new SplFixedArray with duplicate values removed. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray + */ + public static function unique(SplFixedArray $array, bool $strict = true): SplFixedArray + { + $values = self::toArray($array); + + if ($strict) { + $unique = []; + + foreach ($values as $v) { + $found = false; + + foreach ($unique as $u) { + if ($v === $u) { + $found = true; + + break; + } + } + + if (!$found) { + $unique[] = $v; + } + } + } else { + $unique = array_unique($values); + } + + return self::fromArray($unique, false); + } + + + + /** + * Prepends a value to the start of the array. + * + * @param \SplFixedArray $array + * + * @return \SplFixedArray + */ + public static function unshift(SplFixedArray $array, mixed $value): SplFixedArray + { + $count = self::count($array); + + // Increase size by 1 to make space at index 0. + self::setSize($count + 1, $array); + + // Shift all items one slot to the right and insert the new value at index 0. + for ($i = $count - 1; $i >= 0; $i--) { + self::offsetSet($array, $i + 1, self::offsetGet($i, $array)); + } + + self::offsetSet($array, 0, $value); + + return $array; + } } diff --git a/src/FixedArrayable.php b/src/FixedArrayable.php deleted file mode 100644 index 5507393..0000000 --- a/src/FixedArrayable.php +++ /dev/null @@ -1,215 +0,0 @@ -value = $input; - } elseif ($input instanceof Collection) { - $this->value = FixedArray::fromCollection($input); - } else { - $this->value = is_array($input) ? FixedArray::fromArray($input) : FixedArray::fromArray([$input]); - } - } - - /** - * Alias for push. - */ - public function add(mixed $input): self - { - return $this->push($input); - } - - /** - * Adds values from a given array or array-like object into the current fixed array. - */ - public function addFrom(ArrayAccess|array $input): self - { - return $this->from(FixedArray::addFrom($input, $this->value)); - } - - /** - * Returns whether a given item is contained within the array. - */ - public function contains(mixed $item, bool $useStrict = false): bool - { - return FixedArray::contains($item, $this->value, $useStrict); - } - - /** - * Return the number of elements in the fixed array. - */ - public function count(): int - { - return FixedArray::count($this->value); - } - - /** - * Create a new fixed array fluent interface. - */ - public function create(int $count = 5): self - { - return $this->from(FixedArray::create($count)); - } - - /** - * Apply a callback to each item in the array without modifying the original array. - */ - public function each(callable $callback): self - { - FixedArray::each($this->value, $callback); - - return $this; - } - - /** - * Apply a filter to the fixed array. - */ - public function filter(callable $callback): self - { - return $this->from(FixedArray::filter($this->value, $callback)); - } - - /** - * Returns the first element of the array. - */ - public function first(): mixed - { - return FixedArray::first($this->value); - } - - /** - * Create a new fixed array fluent interface from a given input. - */ - public function from(mixed $input): self - { - return new self($input); - } - - /** - * Create a fixed array from a standard array. - */ - public function fromArray(array $array): self - { - return $this->from($array); - } - - /** - * Create a fixed array from a collection. - */ - public function fromCollection(Collection $collection): self - { - return $this->from($collection); - } - - /** - * Retrieve the underlying fixed array value. - */ - public function get(): SplFixedArray - { - return $this->value; - } - - /** - * Return the last element of the array. - */ - public function last(): mixed - { - return FixedArray::last($this->value); - } - - /** - * Apply a callback to each item in the array and return it. - */ - public function map(string|callable $callback): self - { - return $this->from(FixedArray::map($this->value, $callback)); - } - - /** - * Merge the current fixed array with any number of iterable objects or arrays. - */ - public function merge(array|SplFixedArray|Collection ...$inputs): self - { - return $this->from(FixedArray::merge($this->value, $inputs)); - } - - /** - * Pop the last element off the fixed array. - */ - public function pop(): mixed - { - return FixedArray::pop($this->value); - } - - /** - * Push a value onto the fixed array. - */ - public function push(mixed $input): self - { - return $this->from(FixedArray::push($input, $this->value)); - } - - /** - * Resizes the fixed array to the given number of indices. - */ - public function resize(int $indices): self - { - FixedArray::resize($indices, $this->value); - - return $this; - } - - /** - * Returns the second value in the fixed array. - */ - public function second(): mixed - { - return FixedArray::second($this->value); - } - - /** - * Convert the fixed array into a standard PHP array. - */ - public function toArray(): array - { - return FixedArray::toArray($this->value); - } - - /** - * Convert the fixed array to a collection. - */ - public function toCollection(): Collection - { - return FixedArray::toCollection($this->value); - } - - /** - * Get the JSON representation of the arrayable object. - */ - public function jsonSerialize(): array - { - return $this->toArray(); - } -} diff --git a/src/Fluent/FixedArrayable.php b/src/Fluent/FixedArrayable.php new file mode 100644 index 0000000..fcb8982 --- /dev/null +++ b/src/Fluent/FixedArrayable.php @@ -0,0 +1,444 @@ + $data + */ + private function __construct(private SplFixedArray $data) {} + + /** + * Create a new fluent interface. + */ + public static function make(mixed $value, ?int $count = null, bool $preserveKeys = true): self + { + if ($value instanceof self) { + return $value; + } + + if ($value instanceof FixedArray) { + return new self(FixedArray::create()); + } + + if ($value instanceof SplFixedArray) { + return new self($value); + } + + if ($value instanceof Arrayable) { + return new self(FixedArray::fromArray($value->toArray(), preserveKeys: $preserveKeys)); + } + + if (is_array($value)) { + return new self(FixedArray::fromArray($value, preserveKeys: $preserveKeys)); + } + + // If a valid count is provided, create a fixed array of that size and push the value. + if ($count !== null && $count > 0) { + $array = FixedArray::create($count); + FixedArray::offsetSet($array, 0, $value); + + return new self($array); + } + + // Otherwise, just create a fixed array with the single value in it with a single index. + return new self(FixedArray::fromArray([$value])); + } + + /** + * Alias for make. + * + * @see \Petrobolos\FixedArray\Fluent\FixedArrayable::make() + */ + public static function use(mixed $value, ?int $count = null, bool $preserveKeys = true): self + { + return self::make($value, $count, $preserveKeys); + } + + /** + * Alias for push. + * + * @see \Petrobolos\FixedArray\Fluent\FixedArrayable::push() + */ + public function add(mixed $value): self + { + return $this->push($value); + } + + /** + * Add multiple values from an iterable (array, collection, etc.) to the fixed array. + * + * @param iterable $items + */ + public function addFrom(iterable $items): self + { + $this->data = FixedArray::addFrom($this->data, $items); + + return $this; + } + + /** + * Chunk the fixed array into smaller fixed arrays of a given size. + */ + public function chunk(int $size): self + { + $this->data = FixedArray::chunk($this->data, $size); + + return $this; + } + + /** + * Chunk the fixed array into smaller fixed arrays while the given callback returns true. + */ + public function chunkWhile(callable $callback): self + { + $this->data = FixedArray::chunkWhile($this->data, $callback); + + return $this; + } + + /** + * Determine if the fixed array contains a given value. + */ + public function contains(mixed $value, bool $useStrict = true): bool + { + return FixedArray::contains($this->data, $value, $useStrict); + } + + /** + * Use the DSFARGEG algorithm to replace your data with something totally better. + */ + public function dsfargeg(): self + { + $this->data = FixedArray::dsfargeg(); + + return $this; + } + + /** + * Execute a callback for each item in the fixed array. + */ + public function each(callable $callback): self + { + FixedArray::each($this->data, $callback); + + return $this; + } + + /** + * Fill the fixed array with a given value. + */ + public function fill(mixed $value): self + { + $this->data = FixedArray::fill($this->data, $value); + + return $this; + } + + /** + * Find the first item in the fixed array that matches the given callback. + */ + public function find(callable $callback): mixed + { + return FixedArray::find($this->data, $callback); + } + + /** + * Find the key/index of the first item in the fixed array that matches the given callback. + */ + public function findKey(callable $callback): ?int + { + return FixedArray::findKey($this->data, $callback); + } + + /** + * Alias for findKey. + * + * @see \Petrobolos\FixedArray\Fluent\FixedArrayable::findKey() + */ + public function findIndex(callable $callback): ?int + { + return $this->findKey($callback); + } + + /** + * Get the first item in the fixed array. + */ + public function first(): mixed + { + return FixedArray::first($this->data); + } + + /** + * Flatten a multi-dimensional fixed array into a single dimension. + */ + public function flatten(?int $depth = null): self + { + $this->data = FixedArray::flatten($this->data, $depth); + + return $this; + } + + /** + * Get the underlying SplFixedArray instance. + * + * @return \SplFixedArray + */ + public function get(): SplFixedArray + { + return $this->data; + } + + /** + * Get the size of the fixed array. + */ + public function getSize(): int + { + return FixedArray::getSize($this->data); + } + + /** + * Get the last item in the fixed array. + */ + public function last(): mixed + { + return FixedArray::last($this->data); + } + + /** + * Map each item in the fixed array through a callback function. + */ + public function map(callable $callback): self + { + $this->data = FixedArray::map($this->data, $callback); + + return $this; + } + + /** + * Merge one or more arrays or iterables into the fixed array. + * + * @param \SplFixedArray|iterable ...$sources + */ + public function merge(SplFixedArray|iterable ...$sources): self + { + $this->data = FixedArray::merge($this->data, ...$sources); + + return $this; + } + + /** + * Nullify all items in the fixed array. + */ + public function nullify(): self + { + $this->data = FixedArray::nullify($this->data); + + return $this; + } + + /** + * Determine if an index exists in the fixed array. + */ + public function offsetExists(int $index): bool + { + return FixedArray::offsetExists($index, $this->data); + } + + /** + * Get the value at a given index in the fixed array. + */ + public function offsetGet(int $index): mixed + { + return FixedArray::offsetGet($index, $this->data); + } + + /** + * Set the value at a given index in the fixed array. + */ + public function offsetSet(int $index, mixed $value): self + { + FixedArray::offsetSet($this->data, $index, $value); + + return $this; + } + + /** + * Pop and return the last item from the fixed array. + */ + public function pop(): mixed + { + return FixedArray::pop($this->data); + } + + /** + * Pop the last item from the fixed array and append it to the given output array. + * Allows method chaining to continue. + * + * @param array $output + */ + public function popToArray(array &$output): self + { + $output[] = FixedArray::pop($this->data); + + return $this; + } + + /** + * Push a value onto the end of the fixed array. + */ + public function push(mixed $value): self + { + $this->data = FixedArray::push($this->data, $value); + + return $this; + } + + /** + * Get a random item from the fixed array. + * + * @throws \Random\RandomException + */ + public function random(): mixed + { + return FixedArray::random($this->data); + } + + /** + * Alias for setSize. + * + * @see \Petrobolos\FixedArray\Fluent\FixedArrayable::setSize() + */ + public function resize(int $size): self + { + return $this->setSize($size); + } + + /** + * Reverse the order of items in the fixed array. + */ + public function reverse(): self + { + $this->data = FixedArray::reverse($this->data); + + return $this; + } + + /** + * Get the second item in the fixed array. + */ + public function second(): mixed + { + return FixedArray::second($this->data); + } + + /** + * Set the size of the fixed array. + */ + public function setSize(int $size): self + { + FixedArray::setSize($size, $this->data); + + return $this; + } + + /** + * Shift and return the first item from the fixed array. + */ + public function shift(): mixed + { + return FixedArray::shift($this->data); + } + + /** + * Shuffle the items in the fixed array randomly. + */ + public function shuffle(): self + { + $this->data = FixedArray::shuffle($this->data); + + return $this; + } + + /** + * Slice a portion of the fixed array. + */ + public function slice(int $offset, ?int $length = null): self + { + $this->data = FixedArray::slice($this->data, $offset, $length); + + return $this; + } + + /** + * Sort the fixed array using an optional callback for comparison. + */ + public function sort(?callable $callback = null): self + { + $this->data = FixedArray::sort($this->data, $callback); + + return $this; + } + + /** + * Get the instance as a plain array. + * + * @return array + */ + public function toArray(): array + { + return FixedArray::toArray($this->data); + } + + /** + * Get the instance as an Illuminate collection. + * + * @return \Illuminate\Support\Collection + */ + public function toCollection(): Collection + { + return FixedArray::toCollection($this->data); + } + + /** + * Alias for get. + * + * @see \Petrobolos\FixedArray\Fluent\FixedArrayable::get() + * + * @return \SplFixedArray + */ + public function toFixedArray(): SplFixedArray + { + return $this->get(); + } + + /** + * Remove duplicate values from the fixed array. + */ + public function unique(bool $useStrict = true): self + { + $this->data = FixedArray::unique($this->data, $useStrict); + + return $this; + } + + /** + * Unshift a value onto the beginning of the fixed array. + */ + public function unshift(mixed $value): self + { + $this->data = FixedArray::unshift($this->data, $value); + + return $this; + } +} diff --git a/src/FixedArrayFunctionsServiceProvider.php b/src/Providers/FixedArrayServiceProvider.php similarity index 74% rename from src/FixedArrayFunctionsServiceProvider.php rename to src/Providers/FixedArrayServiceProvider.php index 8015ff0..b50eb30 100644 --- a/src/FixedArrayFunctionsServiceProvider.php +++ b/src/Providers/FixedArrayServiceProvider.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Petrobolos\FixedArray; +namespace Petrobolos\FixedArray\Providers; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; -class FixedArrayFunctionsServiceProvider extends PackageServiceProvider +class FixedArrayServiceProvider extends PackageServiceProvider { /** * Configures the package for usage. diff --git a/src/helpers.php b/src/helpers.php index 99ada29..a297da6 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,16 +1,15 @@ assertEquals($count, FixedArray::count($array)); -}); - -test('from array creates a fixed array from a regular array', function () { - $array = ['test']; - $count = count($array); - $convertedArray = FixedArray::fromArray($array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($count, FixedArray::count($convertedArray)); - - /** @phpstan-ignore-next-line */ - $this->assertSame(head($array), FixedArray::first($convertedArray)); -}); - -test('get size returns the size of the array', function () { - $count = 5; - $array = new SplFixedArray($count); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($count, FixedArray::getSize($array)); -}); - -test('offset exists returns whether a given index is occupied', function () { - $array = new SplFixedArray(5); - FixedArray::offsetSet(4, 'test', $array); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::offsetExists(4, $array)); -}); - -test('offset get returns whatever is stored in a given index', function () { - $array = new SplFixedArray(5); - $test = 'test'; - $index = 3; - - FixedArray::offsetSet($index, $test, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($test, FixedArray::offsetGet($index, $array)); - - /** @phpstan-ignore-next-line */ - $this->assertNull(FixedArray::offsetGet(0, $array)); -}); - -test('offset set pushes a given value to a chosen index', function () { - $array = new SplFixedArray(5); - $test = 'test'; - $index = 4; - - FixedArray::offsetSet($index, $test, $array); - - /** @phpstan-ignore-next-line */ - $this->assertSame($test, FixedArray::offsetGet($index, $array)); -}); - -test('set size increases the size of a fixed array', function () { - $originalSize = 5; - $array = new SplFixedArray($originalSize); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($originalSize, FixedArray::getSize($array)); - - $newSize = 10; - FixedArray::setSize($newSize, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($newSize, FixedArray::getSize($array)); -}); - -test('to array converts the fixed array into a standard php array', function () { - $array = new SplFixedArray(5); - $test = 'test'; - FixedArray::push('test', $array); - - $convertedArray = FixedArray::toArray($array); - - /** @phpstan-ignore-next-line */ - $this->assertIsArray($convertedArray); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($test, head($convertedArray)); -}); diff --git a/tests/ArrayMethodTest.php b/tests/ArrayMethodTest.php deleted file mode 100644 index 81fd95b..0000000 --- a/tests/ArrayMethodTest.php +++ /dev/null @@ -1,239 +0,0 @@ -assertTrue(FixedArray::contains(1, $combinedArray)); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(2, $combinedArray)); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(3, $combinedArray)); -}); - -test('contains returns true if a given item is contained within the array', function () { - $item = 'test'; - $array = FixedArray::push($item, FixedArray::create()); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains($item, $array)); - - /** @phpstan-ignore-next-line */ - $this->assertFalse(FixedArray::contains('not in the array', $array)); -}); - -test('create returns a new spl fixed array of a given size', function () { - $count = 10; - $array = FixedArray::create($count); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::isFixedArray($array)); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($count, FixedArray::count($array)); -}); - -test('each applies a callback over each item without modifying the array', function () { - $values = FixedArray::fromArray([1, 2, 3]); - $otherContainer = FixedArray::create(); - - FixedArray::each($values, static function (int $value) use (&$otherContainer) { - FixedArray::push($value * 2, $otherContainer); - }); - - /** @phpstan-ignore-next-line */ - $this->assertNotEquals($values, $otherContainer); - - /** @phpstan-ignore-next-line */ - $this->assertEquals(FixedArray::first($values), 1); - - /** @phpstan-ignore-next-line */ - $this->assertEquals(FixedArray::first($otherContainer), 2); -}); - -test('filter removes items from an array that pass a given test', function () { - $values = FixedArray::fromArray([1, 2, 3, 4, 5]); - - $filtered = FixedArray::filter($values, static fn(int $value) => $value % 2 === 0); - - /** @phpstan-ignore-next-line */ - $this->assertNotContains(1, $filtered); - - /** @phpstan-ignore-next-line */ - $this->assertNotContains(3, $filtered); - - /** @phpstan-ignore-next-line */ - $this->assertNotContains(5, $filtered); -}); - -test('first returns the first element of the array', function () { - $array = new SplFixedArray(2); - $test = 'test'; - - FixedArray::push($test, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($test, FixedArray::first($array)); -}); - - -test('from collection creates a fixed array from a collection', function () { - $collection = collect()->push('item'); - $convertedArray = FixedArray::fromCollection($collection); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($collection->count(), FixedArray::count($convertedArray)); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($collection->first(), FixedArray::first($convertedArray)); -}); - -test('is fixed array indicates whether a given variable is an spl fixed array', function () { - $fixedArray = FixedArray::create(); - $standardArray = []; - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::isFixedArray($fixedArray)); - - /** @phpstan-ignore-next-line */ - $this->assertFalse(FixedArray::isFixedArray($standardArray)); -}); - -test('last retrieves the last value from an array', function () { - $array = FixedArray::fromArray([1, 2, 3, 4, 5]); - - /** @phpstan-ignore-next-line */ - $this->assertEquals(5, FixedArray::last($array)); -}); - -test('map applies a function to an array and returns it', function () { - $array = FixedArray::fromArray([1, 2, 3, 4, 5]); - - $mappedArray = FixedArray::map($array, static fn(int $item) => $item * 2); - - foreach ($mappedArray as $index => $item) { - /** @phpstan-ignore-next-line */ - $this->assertEquals($array[$index] * 2, $item); - } -}); - -test('map applies a function by name to an array and returns it', function () { - $array = FixedArray::fromArray([1, 2, 3, 4, 5]); - - $mappedArray = FixedArray::map($array, 'is_integer'); - - foreach ($mappedArray as $item) { - /** @phpstan-ignore-next-line */ - $this->assertTrue($item); - } -}); - -test('merge will merge together multiple array-like items into a single fixed array', function () { - $fixedArray = FixedArray::fromArray([1]); - $collection = collect(2); - $array = [3]; - $anotherFixedArray = FixedArray::fromArray([4]); - - $newArray = FixedArray::merge($fixedArray, $collection, $array, $anotherFixedArray); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(1, $newArray)); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(2, $newArray)); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(3, $newArray)); - - /** @phpstan-ignore-next-line */ - $this->assertTrue(FixedArray::contains(4, $newArray)); -}); - -test('nullify blanks each element of a fixed array to a null value', function () { - $array = FixedArray::fromArray([1, 2, 3, 4, 5]); - - FixedArray::nullify($array); - - foreach ($array as $item) { - /** @phpstan-ignore-next-line */ - $this->assertNull($item); - } -}); - -test('offset null replaces a given value at a given offset with null', function () { - $array = FixedArray::fromArray([1, 2]); - - FixedArray::offsetNull(1, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals(1, FixedArray::first($array)); - - /** @phpstan-ignore-next-line */ - $this->assertNull(FixedArray::second($array)); -}); - -test('pop removes the latest value from an array and nullifies it in the process', function () { - $array = FixedArray::fromArray([1, 2, 3]); - - $value = FixedArray::pop($array); - - /** @phpstan-ignore-next-line */ - $this->assertSame(3, $value); - - /** @phpstan-ignore-next-line */ - $this->assertNull(FixedArray::last($array)); -}); - -test('push sets a value to the first available space on an empty array', function () { - $array = FixedArray::create(); - $expected = 'test'; - - FixedArray::push($expected, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($expected, FixedArray::first($array)); -}); - -test('push will increase the size of an array by one if more space is needed to push an element to it', function () { - $array = FixedArray::fromArray([1, 2, 3, 4, 5]); - - $expectedSizeAfterResize = 6; - $expected = 6; - - FixedArray::push($expected, $array); - /** @phpstan-ignore-next-line */ - $this->assertEquals($expectedSizeAfterResize, FixedArray::count($array)); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($expected, FixedArray::last($array)); -}); - -test('push will occupy a null value in the middle of the array if it is the first available element', function () { - $array = FixedArray::fromArray([1, null, 2, 3, 4, 5, 6]); - $expected = 'test'; - - FixedArray::push($expected, $array); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($expected, FixedArray::second($array)); -}); - -test('to collection produces a collection from a given fixed array', function () { - $collection = collect([1, 2, 3]); - $fixedArray = FixedArray::fromCollection($collection); - $newCollection = FixedArray::toCollection($fixedArray); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($collection, $newCollection); -}); diff --git a/tests/Feature/ArchitectureTest.php b/tests/Feature/ArchitectureTest.php new file mode 100644 index 0000000..e9cd6d3 --- /dev/null +++ b/tests/Feature/ArchitectureTest.php @@ -0,0 +1,32 @@ +preset()->php(); +arch()->preset()->laravel(); +arch()->preset()->security(); + +arch() + ->expect('Petrobolos\FixedArray') + ->toHaveMethodsDocumented() + ->toHavePropertiesDocumented() + ->toUseStrictEquality() + ->toUseStrictTypes(); + +arch() + ->expect('Petrobolos\FixedArray\Facades') + ->toHaveMethod('getFacadeAccessor'); + +arch() + ->expect('Petrobolos\FixedArray\Providers') + ->toExtend(ServiceProvider::class) + ->toHaveSuffix('ServiceProvider'); + +arch() + ->expect('Petrobolos\FixedArray\Tests') + ->toHaveMethodsDocumented() + ->toHavePropertiesDocumented() + ->toUseStrictEquality() + ->toUseStrictTypes(); diff --git a/tests/Feature/FixedArrayFacadeTest.php b/tests/Feature/FixedArrayFacadeTest.php new file mode 100644 index 0000000..07c9c0d --- /dev/null +++ b/tests/Feature/FixedArrayFacadeTest.php @@ -0,0 +1,11 @@ +toBeInstanceOf(SplFixedArray::class); +}); diff --git a/tests/FixedArrayableTest.php b/tests/FixedArrayableTest.php deleted file mode 100644 index fb860e7..0000000 --- a/tests/FixedArrayableTest.php +++ /dev/null @@ -1,12 +0,0 @@ -addFrom([4, 5, 6]); - - $expected = FixedArray::fromArray([1, 2, 3, 4, 5, 6]); - - /** @phpstan-ignore-next-line */ - $this->assertEquals($expected, $fluent->get()); -}); diff --git a/tests/Pest.php b/tests/Pest.php index d6a2bee..ff79bee 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php index af5fc9d..f780e91 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,23 +1,25 @@ set('database.default', 'testing'); + $app['config']->set('database.default', 'testing'); } /** @inheritDoc */ protected function getPackageProviders($app): array { return [ - FixedArrayFunctionsServiceProvider::class, + FixedArrayServiceProvider::class, ]; } } diff --git a/tests/Unit/Facades/FixedArrayTest.php b/tests/Unit/Facades/FixedArrayTest.php new file mode 100644 index 0000000..6a9e200 --- /dev/null +++ b/tests/Unit/Facades/FixedArrayTest.php @@ -0,0 +1,22 @@ +getFacade()) + ->toBeClass() + ->toEqual(BaseFixedArray::class); +}); diff --git a/tests/Unit/FixedArrayTest.php b/tests/Unit/FixedArrayTest.php new file mode 100644 index 0000000..dc568a7 --- /dev/null +++ b/tests/Unit/FixedArrayTest.php @@ -0,0 +1,1424 @@ +toArray()) + ->toEqual([1, 2, 3]) + ->and($result)->toBe($array); + }); +}); + +describe('add from', function (): void { + it('adds values from a PHP array', function (): void { + $array = FixedArray::fromArray([1, 2]); + $result = FixedArray::addFrom($array, [3, 4]); + + expect($array->toArray()) + ->toEqual([1, 2, 3, 4]) + ->and($result) + ->toBe($array); + }); + + it('adds values from another SplFixedArray', function (): void { + $array = FixedArray::fromArray([1]); + $source = FixedArray::fromArray([2, 3]); + $result = FixedArray::addFrom($array, $source); + + expect($array->toArray()) + ->toEqual([1, 2, 3]) + ->and($result) + ->toBe($array); + }); + + it('adds values from a collection', function (): void { + $array = FixedArray::fromArray([1]); + $source = collect([2, 3]); + $result = FixedArray::addFrom($array, $source); + + expect($array->toArray()) + ->toEqual([1, 2, 3]) + ->and($result) + ->toBe($array); + }); + + it('works with empty iterable', function (): void { + $array = FixedArray::fromArray([1, 2]); + $result = FixedArray::addFrom($array, []); + + expect($array->toArray()) + ->toEqual([1, 2]) + ->and($result)->toBe($array); + }); + + it('supports mixed types', function (): void { + $array = FixedArray::fromArray([1]); + $source = ['foo', null, true]; + $result = FixedArray::addFrom($array, $source); + + expect($array->toArray()) + ->toEqual([1, 'foo', null, true]) + ->and($result) + ->toBe($array); + }); +}); + +describe('chunk', function (): void { + it('splits a fixed array into evenly sized chunks', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5, 6]); + $result = FixedArray::chunk($array, 2); + + expect($result)->toBeInstanceOf(SplFixedArray::class) + ->and($result->count())->toBe(3) + ->and(FixedArray::toArray($result[0]))->toBe([1, 2]) + ->and(FixedArray::toArray($result[1]))->toBe([3, 4]) + ->and(FixedArray::toArray($result[2]))->toBe([5, 6]); + }); + + it('handles arrays not evenly divisible by the chunk size', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $result = FixedArray::chunk($array, 2); + + expect(FixedArray::toArray($result[0])) + ->toBe([1, 2]) + ->and(FixedArray::toArray($result[1]))->toBe([3, 4]) + ->and(FixedArray::toArray($result[2]))->toBe([5]); + }); + + it('returns an empty fixed array for empty input', function (): void { + $array = FixedArray::create(0); + $result = FixedArray::chunk($array, 2); + + expect($result) + ->toBeInstanceOf(SplFixedArray::class) + ->and($result->count())->toBe(0); + }); + + it('throws an exception for non-positive chunk size', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::chunk($array, 0); + })->throws(InvalidArgumentException::class); +}); + +describe('chunk while', function (): void { + it('chunks consecutive increasing numbers together', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 7, 8, 10, 11, 12]); + $result = FixedArray::chunkWhile($array, function (int $value, int $key, ?int $previous): bool { + return $previous !== null && $value === $previous + 1; + }); + + expect($result->count()) + ->toBe(3) + ->and(FixedArray::toArray($result[0]))->toBe([1, 2, 3]) + ->and(FixedArray::toArray($result[1]))->toBe([7, 8]) + ->and(FixedArray::toArray($result[2]))->toBe([10, 11, 12]); + }); + + it('creates single-item chunks when callback always returns false', function (): void { + $array = FixedArray::fromArray(['a', 'b', 'c']); + $result = FixedArray::chunkWhile($array, fn(): false => false); + + expect($result->count()) + ->toBe(3) + ->and(FixedArray::toArray($result[0]))->toBe(['a']) + ->and(FixedArray::toArray($result[1]))->toBe(['b']) + ->and(FixedArray::toArray($result[2]))->toBe(['c']); + }); + + it('creates one full chunk when callback always returns true', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::chunkWhile($array, fn(): true => true); + + expect($result->count()) + ->toBe(1) + ->and(FixedArray::toArray($result[0])) + ->toBe([1, 2, 3]); + }); + + it('handles an empty array gracefully', function (): void { + $array = FixedArray::create(0); + $result = FixedArray::chunkWhile($array, fn(): true => true); + + expect($result->count())->toBe(0); + }); +}); + +describe('contains', function (): void { + it('returns true if the item exists in the array (strict)', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + expect(FixedArray::contains($array, 2))->toBeTrue(); + }); + + it('returns false if the item does not exist (strict)', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + expect(FixedArray::contains($array, 4))->toBeFalse(); + }); + + it('respects strict parameter (type check)', function (): void { + $array = FixedArray::fromArray([1, '2', 3]); + + expect(FixedArray::contains($array, 2)) + ->toBeFalse() + ->and(FixedArray::contains($array, 2, false)) + ->toBeTrue(); + }); + + it('works with mixed types and null', function (): void { + $array = FixedArray::fromArray([1, null, 'foo', true]); + + expect(FixedArray::contains($array, null)) + ->toBeTrue() + ->and(FixedArray::contains($array, 'foo')) + ->toBeTrue() + ->and(FixedArray::contains($array, false)) + ->toBeFalse(); + }); + + it('returns false for array with no indices', function (): void { + $array = new SplFixedArray(0); + expect(FixedArray::contains($array, 1))->toBeFalse(); + }); +}); + +describe('create', function (): void { + it('creates a SplFixedArray with default size', function (): void { + $array = FixedArray::create(); + + expect($array) + ->toBeInstanceOf(SplFixedArray::class) + ->and($array->getSize()) + ->toBe(5) + ->and($array->toArray()) + ->toEqual([null, null, null, null, null]); + }); + + it('creates a SplFixedArray with a custom size', function (): void { + $array = FixedArray::create(3); + + expect($array->getSize()) + ->toBe(3) + ->and($array->toArray()) + ->toEqual([null, null, null]); + }); + + it('creates an empty array if size is zero', function (): void { + $array = FixedArray::create(0); + + expect($array->getSize()) + ->toBe(0) + ->and($array->toArray()) + ->toEqual([]); + }); + + it('throws an error if negative size is provided', function (): void { + FixedArray::create(-1); + })->throws(ValueError::class); +}); + +describe('count', function (): void { + it('returns the correct count for a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $count = FixedArray::count($array); + + expect($count)->toBe(3); + }); + + it('returns 0 for an empty array', function (): void { + $array = new SplFixedArray(0); + $count = FixedArray::count($array); + + expect($count)->toBe(0); + }); + + it('counts correctly after resizing', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::setSize(5, $array); + $count = FixedArray::count($array); + + expect($count)->toBe(5); + }); +}); + +describe('dsfargeg', function (): void { + it('returns a fixed array with "dsfargeg" letters', function (): void { + $mukku = FixedArray::dsfargeg(); + + expect(FixedArray::toArray($mukku)) + ->toBe(['D', 'S', 'F', 'A', 'R', 'G', 'E', 'G']) + ->and($mukku)->toBeInstanceOf(SplFixedArray::class); + }); +}); + +describe('each', function (): void { + it('iterates over all items and calls the callback with value and key', function (): void { + $array = FixedArray::fromArray([10, 20, 30]); + $collected = []; + + $result = FixedArray::each($array, function (int $value, int $key) use (&$collected): void { + $collected[$key] = $value * 2; + }); + + expect($collected) + ->toEqual([0 => 20, 1 => 40, 2 => 60]) + ->and($result->toArray()) + ->toEqual([10, 20, 30]); // original array unchanged + }); + + it('works with array with no indices', function (): void { + $array = new SplFixedArray(0); + $collected = []; + + $result = FixedArray::each($array, function (mixed $value, int $key) use (&$collected): void { + $collected[$key] = $value; + }); + + expect($collected) + ->toEqual([]) + ->and($result->count()) + ->toBe(0); + }); + + it('supports mixed types', function (): void { + $array = FixedArray::fromArray([1, null, 'foo', true]); + $collected = []; + + FixedArray::each($array, function (mixed $value, int $key) use (&$collected): void { + $collected[$key] = $value === null ? 'null' : (string) $value; + }); + + expect($collected) + ->toEqual([0 => '1', 1 => 'null', 2 => 'foo', 3 => '1']) + ->and($array->toArray()) + ->toEqual([1, null, 'foo', true]); // original array unchanged + }); +}); + +describe('fill', function (): void { + it('fills the array with the given value', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $filled = FixedArray::fill($array, 0); + + expect(FixedArray::toArray($filled))->toBe([0, 0, 0]); + }); + + it('handles empty arrays gracefully', function (): void { + $array = FixedArray::create(0); + $filled = FixedArray::fill($array, 'x'); + + expect(FixedArray::count($filled))->toBe(0) + ->and(FixedArray::toArray($filled))->toBe([]); + }); +}); + +describe('filter', function (): void { + it('filters values based on a callback', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4]); + $filtered = FixedArray::filter($array, fn(int $v): bool => $v % 2 === 0); + + expect($filtered->toArray())->toEqual([2, 4]); + }); + + it('returns an empty array when no items match', function (): void { + $array = FixedArray::fromArray([1, 3, 5]); + $filtered = FixedArray::filter($array, fn(int $v): bool => $v % 2 === 0); + + expect($filtered->count()) + ->toBe(0) + ->and($filtered->toArray()) + ->toEqual([]); + }); + + it('works with mixed types and nulls', function (): void { + $array = FixedArray::fromArray([1, null, 'foo', '', false]); + $filtered = FixedArray::filter($array, fn(int|string|bool|null $v): bool => !empty($v)); + + expect($filtered->toArray())->toEqual([1, 'foo']); + }); + + it('handles an array wtih no indices', function (): void { + $array = new SplFixedArray(0); + $filtered = FixedArray::filter($array, fn(mixed $v): true => true); + + expect($filtered->count())->toBe(0); + }); +}); + +describe('find', function (): void { + it('returns the first matching element', function (): void { + $array = FixedArray::fromArray([1, 3, 5, 8, 10]); + + $result = FixedArray::find($array, fn(int $v): bool => $v % 2 === 0); + + expect($result)->toBe(8); + }); + + it('returns null if no element matches', function (): void { + $array = FixedArray::fromArray([1, 3, 5]); + + $result = FixedArray::find($array, fn(int $v): bool => $v > 10); + + expect($result)->toBeNull(); + }); + + it('can use key in the callback', function (): void { + $array = FixedArray::fromArray(['a', 'b', 'c']); + + $result = FixedArray::find($array, fn(string $v, int $k): bool => $k === 1); + + expect($result)->toBe('b'); + }); + + it('returns null for empty array', function (): void { + $array = FixedArray::create(0); + + $result = FixedArray::find($array, fn(): true => true); + + expect($result)->toBeNull(); + }); +}); + +describe('findKey', function (): void { + it('returns the index of the first matching element', function (): void { + $array = FixedArray::fromArray([1, 3, 5, 8, 10]); + + $result = FixedArray::findKey($array, fn(int $v): bool => $v % 2 === 0); + + expect($result)->toBe(3); + }); + + it('returns null if no element matches', function (): void { + $array = FixedArray::fromArray([1, 3, 5]); + + $result = FixedArray::findKey($array, fn(int $v): bool => $v > 10); + + expect($result)->toBeNull(); + }); + + it('can use both key and value in callback', function (): void { + $array = FixedArray::fromArray(['a', 'b', 'c']); + + $result = FixedArray::findKey($array, fn(string $v, int $k): bool => $v === 'b' && $k === 1); + + expect($result)->toBe(1); + }); + + it('returns null for empty arrays', function (): void { + $array = FixedArray::create(0); + + $result = FixedArray::findKey($array, fn(): true => true); + + expect($result)->toBeNull(); + }); +}); + +describe('findIndex', function (): void { + it('acts as an alias for findKey', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + + $keyFromFindKey = FixedArray::findKey($array, fn(int $v): bool => $v === 2); + $keyFromFindIndex = FixedArray::findIndex($array, fn(int $v): bool => $v === 2); + + expect($keyFromFindIndex)->toBe($keyFromFindKey); + }); +}); + +describe('first', function (): void { + it('returns the first item of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::first($array); + + expect($value)->toBe(1); + }); + + it('returns null if the first item is null', function (): void { + $array = FixedArray::fromArray([null, 2, 3]); + $value = FixedArray::first($array); + + expect($value)->toBeNull(); + }); + + it('throws exception if array has no indices', function (): void { + $array = new SplFixedArray(0); + FixedArray::first($array); + })->throws(RuntimeException::class); +}); + +describe('flatten', function (): void { + it('flattens a single-level nested fixed array', function (): void { + $nested = FixedArray::fromArray([ + 1, + FixedArray::fromArray([2, 3]), + 4, + ]); + + $result = FixedArray::flatten($nested); + + expect(FixedArray::toArray($result))->toBe([1, 2, 3, 4]); + }); + + it('flattens deeply nested fixed arrays', function (): void { + $nested = FixedArray::fromArray([ + 1, + FixedArray::fromArray([ + 2, + FixedArray::fromArray([3, 4]), + ]), + 5, + ]); + + $result = FixedArray::flatten($nested); + + expect(FixedArray::toArray($result))->toBe([1, 2, 3, 4, 5]); + }); + + it('flattens only up to the given depth', function (): void { + $nested = FixedArray::fromArray([ + 1, + FixedArray::fromArray([ + 2, + FixedArray::fromArray([3, 4]), + ]), + 5, + ]); + + $result = FixedArray::flatten($nested, 1); + + $expected = [1, 2, FixedArray::fromArray([3, 4]), 5]; + + expect(array_map( + fn(int|SplFixedArray $item): array|int => $item instanceof SplFixedArray + ? FixedArray::toArray($item) + : $item, + FixedArray::toArray($result), + ))->toBe(array_map( + fn(int|SplFixedArray $item): array|int => $item instanceof SplFixedArray + ? FixedArray::toArray($item) + : $item, + $expected, + )); + + }); + + it('handles empty arrays gracefully', function (): void { + $array = FixedArray::create(0); + + $result = FixedArray::flatten($array); + + expect(FixedArray::count($result))->toBe(0); + }); + + it('handles mixed types and non-iterables safely', function (): void { + $nested = FixedArray::fromArray([ + 1, + 'foo', + FixedArray::fromArray([true, null]), + 5.5, + ]); + + $result = FixedArray::flatten($nested); + + expect(FixedArray::toArray($result))->toBe([1, 'foo', true, null, 5.5]); + }); + + it('flattens a mix of scalars, arrays, collections, and fixed arrays', function (): void { + $array = FixedArray::fromArray([ + 1, + [2, 3], + collect([4, 5]), + FixedArray::fromArray([6, [7, 8]]), + 9, + ]); + + $result = FixedArray::flatten($array); + + // Convert any nested SplFixedArrays to plain arrays for comparison. + $normalized = array_map( + fn(int|SplFixedArray $item): array|int => $item instanceof SplFixedArray + ? FixedArray::toArray($item) + : $item, + FixedArray::toArray($result), + ); + + expect($normalized)->toBe([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + +}); + +describe('fluent', function (): void { + it('creates a fluent interface from a PHP array', function (): void { + $array = [1, 2, 3]; + $fluent = FixedArray::fluent($array); + + expect($fluent)->toBeInstanceOf(FixedArrayable::class); + }); + + it('creates a fluent interface from a collection', function (): void { + $collection = collect([1, 2, 3]); + $fluent = FixedArray::fluent($collection); + + expect($fluent)->toBeInstanceOf(FixedArrayable::class); + }); + + it('creates a fluent interface from an SplFixedArray', function (): void { + $fixed = FixedArray::fromArray([1, 2, 3]); + $fluent = FixedArray::fluent($fixed); + + expect($fluent)->toBeInstanceOf(FixedArrayable::class); + }); +}); + +describe('from array', function (): void { + it('imports a non-empty PHP array into a SplFixedArray', function (): void { + $array = [1, 2, 3]; + $fixed = FixedArray::fromArray($array); + + expect($fixed) + ->toBeInstanceOf(SplFixedArray::class) + ->and($fixed->toArray()) + ->toEqual([1, 2, 3]); + }); + + it('imports an empty PHP array', function (): void { + $array = []; + $fixed = FixedArray::fromArray($array); + + expect($fixed->count())->toBe(0); + }); + + it('preserves numeric keys by default', function (): void { + $array = [0 => 'a', 2 => 'b', 5 => 'c']; + $fixed = FixedArray::fromArray($array); + + // SplFixedArray will always “fill in the gaps” when numeric keys are preserved. + expect($fixed->toArray())->toEqual([ + 0 => 'a', + 1 => null, + 2 => 'b', + 3 => null, + 4 => null, + 5 => 'c', + ]); + + }); + + it('can discard original keys when preserveKeys is false', function (): void { + $array = [0 => 'a', 2 => 'b', 5 => 'c']; + $fixed = FixedArray::fromArray($array, false); + + expect($fixed->toArray())->toEqual(['a', 'b', 'c']); + }); +}); + +describe('from collection', function (): void { + it('imports a non-empty collection into a SplFixedArray', function (): void { + $collection = collect([1, 2, 3]); + $fixed = FixedArray::fromCollection($collection); + + expect($fixed->toArray())->toEqual([1, 2, 3]); + }); + + it('imports an empty collection', function (): void { + $collection = collect(); + $fixed = FixedArray::fromCollection($collection); + + expect($fixed->count())->toBe(0); + }); + + it('preserves keys by default', function (): void { + $collection = collect([0 => 'a', 2 => 'b', 5 => 'c']); + $fixed = FixedArray::fromCollection($collection); + + expect($fixed->toArray())->toEqual([ + 0 => 'a', + 1 => null, + 2 => 'b', + 3 => null, + 4 => null, + 5 => 'c', + ]); + }); + + it('can discard keys when preserveKeys is false', function (): void { + $collection = collect([0 => 'a', 2 => 'b', 5 => 'c']); + $fixed = FixedArray::fromCollection($collection, false); + + expect($fixed->toArray())->toEqual(['a', 'b', 'c']); + }); +}); + +describe('get size', function (): void { + it('returns the correct size for a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $size = FixedArray::getSize($array); + + expect($size)->toBe(3); + }); + + it('returns 0 for an array with no indices', function (): void { + $array = new SplFixedArray(0); + $size = FixedArray::getSize($array); + + expect($size)->toBe(0); + }); + + it('returns the correct size for an empty array with allocated size', function (): void { + $array = new SplFixedArray(5); + $size = FixedArray::getSize($array); + + expect($size)->toBe(5); + }); + + it('returns the updated size after resizing', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::setSize(5, $array); + $size = FixedArray::getSize($array); + + expect($size)->toBe(5); + }); +}); + +describe('is fixed array', function (): void { + it('returns true for an SplFixedArray', function (): void { + $array = new SplFixedArray(3); + $result = FixedArray::isFixedArray($array); + + expect($result)->toBeTrue(); + }); + + it('returns false for a PHP array', function (): void { + $array = [1, 2, 3]; + $result = FixedArray::isFixedArray($array); + + expect($result)->toBeFalse(); + }); + + it('returns false for null', function (): void { + $result = FixedArray::isFixedArray(null); + + expect($result)->toBeFalse(); + }); + + it('returns false for objects that are not SplFixedArray', function (): void { + $result = FixedArray::isFixedArray(new stdClass()); + + expect($result)->toBeFalse(); + }); +}); + +describe('last', function (): void { + it('returns the last item of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::last($array); + + expect($value)->toBe(3); + }); + + it('returns null if the array is empty', function (): void { + $array = new SplFixedArray(3); + $value = FixedArray::last($array); + + expect($value)->toBeNull(); + }); + + it('throws exception for array with no indices', function (): void { + $array = new SplFixedArray(0); + FixedArray::last($array); + })->throws(RuntimeException::class); +}); + +describe('map', function (): void { + it('applies a callback to each element', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $mapped = FixedArray::map($array, fn($v) => $v * 2); + + expect($mapped->toArray())->toEqual([2, 4, 6]); + }); + + it('returns an empty array when mapping over an empty array', function (): void { + $array = new SplFixedArray(0); + $mapped = FixedArray::map($array, fn(int $v): int => $v * 2); + + expect($mapped->toArray()) + ->toEqual([]) + ->and($mapped->count())->toBe(0); + }); + + it('preserves types for mixed values', function (): void { + $array = FixedArray::fromArray([1, 'two', null, true]); + $mapped = FixedArray::map($array, fn(int|string|bool|null $v): string => (string) $v); + + expect($mapped->toArray())->toEqual(['1', 'two', '', '1']); + }); +}); + +describe('merge', function (): void { + it('merges multiple arrays and preserves null values', function (): void { + $target = FixedArray::fromArray([1, 'foo']); + $source = ['bar', null, true]; + + $merged = FixedArray::merge($target, $source); + + expect($merged->toArray())->toEqual([1, 'foo', 'bar', null, true]); + }); + + it('merges SplFixedArrays, PHP arrays, and collections', function (): void { + $target = FixedArray::fromArray([1]); + $source1 = [2, 3]; + $source2 = FixedArray::fromArray([4]); + $source3 = collect([5, 6, null]); + + $merged = FixedArray::merge($target, $source1, $source2, $source3); + + expect($merged->toArray())->toEqual([1, 2, 3, 4, 5, 6, null]); + }); + + it('returns the original target after merge', function (): void { + $target = FixedArray::fromArray([1]); + $source = [2, 3]; + + $result = FixedArray::merge($target, $source); + + expect($result)->toBe($target); // merged into same instance + }); +}); + +describe('nullify', function (): void { + it('replaces all values with null in a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 'foo', true]); + FixedArray::nullify($array); + + expect($array->toArray())->toEqual([null, null, null]); + }); + + it('works on an array without indices', function (): void { + $array = new SplFixedArray(0); + FixedArray::nullify($array); + + expect($array->toArray())->toEqual([]); + }); + + it('preserves array size while nullifying', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::nullify($array); + + expect($array->getSize())->toBe(3); + }); +}); + +describe('offset exists', function (): void { + it('returns true for an existing index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $exists = FixedArray::offsetExists(1, $array); + + expect($exists)->toBeTrue(); + }); + + it('returns false for a non-existing index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $exists = FixedArray::offsetExists(5, $array); + + expect($exists)->toBeFalse(); + }); + + it('returns false if the value at the given index is null', function (): void { + $array = FixedArray::fromArray([1, null, 3]); + $exists = FixedArray::offsetExists(1, $array); + + expect($exists)->toBeFalse(); + }); + + it('returns true if the value at the given index is false but not null', function (): void { + $array = FixedArray::fromArray([1, false, 3]); + $exists = FixedArray::offsetExists(1, $array); + + expect($exists)->toBeTrue(); + }); +}); + +describe('offset get', function (): void { + it('retrieves a value at a valid index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::offsetGet(1, $array); + + expect($value)->toBe(2); + }); + + it('returns null if the value is null', function (): void { + $array = FixedArray::fromArray([1, null, 3]); + $value = FixedArray::offsetGet(1, $array); + + expect($value)->toBeNull(); + }); + + it('throws exception when index is out of bounds', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetGet(5, $array); + })->throws(RuntimeException::class); +}); + +describe('offset null', function (): void { + it('sets a value to null at a valid index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetNull(1, $array); + + expect($array[1]) + ->toBeNull() + ->and($array->toArray()) + ->toEqual([1, null, 3]); + }); + + it('throws exception when index is out of bounds', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetNull(5, $array); + })->throws(RuntimeException::class); +}); + +describe('offset set', function (): void { + it('sets a value at a valid index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetSet($array, 1, 42); + + expect($array[1]) + ->toBe(42) + ->and($array->toArray()) + ->toEqual([1, 42, 3]); + }); + + it('overwrites a value at an existing index', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetSet($array, 0, 'foo'); + + expect($array[0])->toBe('foo'); + }); + + it('throws exception when index is out of bounds', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::offsetSet($array, 5, 99); + })->throws(RuntimeException::class); + + it('supports setting mixed types', function (): void { + $array = FixedArray::fromArray([null, null, null]); + FixedArray::offsetSet($array, 2, [1, 2, 3]); + + expect($array[2])->toEqual([1, 2, 3]); + }); +}); + +describe('pop', function (): void { + it('removes and returns the last item of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::pop($array); + + expect($value)->toBe(3) + ->and($array->toArray()) + ->toEqual([1, 2, null]); + }); + + it('returns null for an array with no indices', function (): void { + $array = new SplFixedArray(0); + $value = FixedArray::pop($array); + + expect($value)->toBeNull() + ->and($array->toArray()) + ->toEqual([]); + }); + + it('works when the last value is null', function (): void { + $array = FixedArray::fromArray([1, null]); + $value = FixedArray::pop($array); + + expect($value)->toBeNull() + ->and($array->toArray()) + ->toEqual([1, null]); + }); + + it('preserves array size but nulls out the last slot', function (): void { + $array = FixedArray::fromArray([1, 2]); + FixedArray::pop($array); + + expect($array->getSize()) + ->toBe(2) + ->and($array[1]) + ->toBeNull(); + }); +}); + +describe('push', function (): void { + it('adds a value to the end of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::push($array, 4); + + expect($array->toArray()) + ->toEqual([1, 2, 3, 4]) + ->and($result)->toBe($array); + }); + + it('adds a value to an empty array', function (): void { + $array = new SplFixedArray(0); + $result = FixedArray::push($array, 1); + + expect($array->toArray()) + ->toEqual([1]) + ->and($result)->toBe($array); + }); + + it('preserves existing null values', function (): void { + $array = FixedArray::fromArray([1, null, 3]); + $result = FixedArray::push($array, 4); + + expect($array->toArray()) + ->toEqual([1, null, 3, 4]) + ->and($result)->toBe($array); + }); + + it('supports mixed types', function (): void { + $array = FixedArray::fromArray([1, 'foo']); + $result = FixedArray::push($array, [1, 2]); + + expect($array->toArray()) + ->toEqual([1, 'foo', [1, 2]]) + ->and($result)->toBe($array); + }); +}); + +describe('random', function (): void { + it('returns an element from a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4]); + $value = FixedArray::random($array); + + expect($value)->toBeIn([1, 2, 3, 4]); + }); + + it('returns null for empty array', function (): void { + $array = FixedArray::create(0); + expect(FixedArray::random($array))->toBeNull(); + }); +}); + +describe('resize', function (): void { + it('resizes the array (alias for setSize)', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::resize(5, $array); + + expect($result)->toBeTrue() + ->and($array->toArray())->toEqual([1, 2, 3, null, null]); + }); + + it('can shrink the array', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $result = FixedArray::resize(2, $array); + + expect($result)->toBeTrue() + ->and($array->toArray()) + ->toEqual([1, 2]); + }); + + it('throws an error on negative size', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + FixedArray::resize(-1, $array); + })->throws(ValueError::class); +}); + +describe('reverse', function (): void { + it('reverses a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $reversed = FixedArray::reverse($array); + + expect($reversed->toArray())->toEqual([3, 2, 1]); + }); + + it('works on an array with zero indices', function (): void { + $array = new SplFixedArray(0); + $reversed = FixedArray::reverse($array); + + expect($reversed->count()) + ->toBe(0) + ->and($reversed->toArray()) + ->toEqual([]); + }); + + it('does not modify the original array', function (): void { + $array = FixedArray::fromArray([1, 2]); + $reversed = FixedArray::reverse($array); + + expect($array->toArray()) + ->toEqual([1, 2]) + ->and($reversed->toArray()) + ->toEqual([2, 1]); + }); + + it('supports mixed types and nulls', function (): void { + $array = FixedArray::fromArray([null, 'a', 1, true]); + $reversed = FixedArray::reverse($array); + + expect($reversed->toArray())->toEqual([true, 1, 'a', null]); + }); +}); + +describe('second', function (): void { + it('returns the second item of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::second($array); + + expect($value)->toBe(2); + }); + + it('returns null if the second item is null', function (): void { + $array = FixedArray::fromArray([1, null, 3]); + $value = FixedArray::second($array); + + expect($value)->toBeNull(); + }); + + it('returns null if there is no second item', function (): void { + $array = FixedArray::fromArray([1]); + $value = FixedArray::second($array); + + expect($value)->toBeNull(); + }); + + it('works with an array with no indices', function (): void { + $array = new SplFixedArray(0); + $value = FixedArray::second($array); + + expect($value)->toBeNull(); + }); +}); + +describe('set size', function (): void { + it('can increase the size of an SplFixedArray', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::setSize(5, $array); + + expect($result) + ->toBeTrue() + ->and($array->toArray()) + ->toHaveCount(5) + ->and($array[3]) + ->toBeNull() + ->and($array[4]) + ->toBeNull(); + }); + + it('can decrease the size of an SplFixedArray', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $result = FixedArray::setSize(3, $array); + + expect($result) + ->toBeTrue() + ->and($array->toArray()) + ->toHaveCount(3) + ->and($array->toArray()) + ->toEqual([1, 2, 3]); + }); + + it('handles empty SplFixedArray', function (): void { + $array = new SplFixedArray(0); + $result = FixedArray::setSize(2, $array); + + expect($result) + ->toBeTrue() + ->and($array->toArray()) + ->toHaveCount(2); + }); + + it('does not allow negative sizes', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + + FixedArray::setSize(-1, $array); + })->throws(ValueError::class); + + it('handles setting the same size', function (): void { + $array = SplFixedArray::fromArray([1, 2, 3]); + $result = FixedArray::setSize(3, $array); + + expect($result) + ->toBeTrue() + ->and($array->toArray()) + ->toEqual([1, 2, 3]); + }); +}); + +describe('shift', function (): void { + it('removes and returns the first item of a non-empty array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $value = FixedArray::shift($array); + + expect($value) + ->toBe(1) + ->and($array->toArray()) + ->toEqual([2, 3, null]); + }); + + it('returns null for array with no indices', function (): void { + $array = new SplFixedArray(0); + $value = FixedArray::shift($array); + + expect($value) + ->toBeNull() + ->and($array->toArray()) + ->toEqual([]); + }); + + it('works when the first value is null', function (): void { + $array = FixedArray::fromArray([null, 2, 3]); + $value = FixedArray::shift($array); + + expect($value) + ->toBeNull() + ->and($array->toArray()) + ->toEqual([2, 3, null]); + }); + + it('preserves array size after shift', function (): void { + $array = FixedArray::fromArray([1, 2]); + FixedArray::shift($array); + + expect($array->getSize()) + ->toBe(2) + ->and($array[1]) + ->toBeNull(); + }); +}); + +describe('shuffle', function (): void { + it('returns a shuffled fixed array containing the same elements', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $shuffled = FixedArray::shuffle($array); + + // The order may change, but all elements must still exist! + expect(FixedArray::toArray($shuffled)) + ->toHaveCount(5) + ->and(array_diff(FixedArray::toArray($array), FixedArray::toArray($shuffled))) + ->toBeEmpty(); + }); + + it('returns an empty array when input is empty', function (): void { + $array = FixedArray::create(0); + $shuffled = FixedArray::shuffle($array); + + expect(FixedArray::count($shuffled)) + ->toBe(0) + ->and(FixedArray::toArray($shuffled)) + ->toBe([]); + }); + + it('returns a new array without modifying the original', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + + // Keep shuffling until the order actually changes (to avoid false positives in tests). + do { + $shuffled = FixedArray::shuffle($array); + } while (FixedArray::toArray($shuffled) === FixedArray::toArray($array)); + + expect(FixedArray::toArray($array)) + ->toBe([1, 2, 3]) + ->and(FixedArray::toArray($shuffled)) + ->not() + ->toBe([1, 2, 3]); + }); +}); + +describe('slice', function (): void { + it('returns a portion of the array', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $sliced = FixedArray::slice($array, 1, 3); + + expect($sliced->toArray())->toEqual([2, 3, 4]); + }); + + it('returns from offset to end if length is null', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4]); + $sliced = FixedArray::slice($array, 2); + + expect($sliced->toArray())->toEqual([3, 4]); + }); + + it('works with negative offset', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4]); + $sliced = FixedArray::slice($array, -2); + + expect($sliced->toArray())->toEqual([3, 4]); + }); + + it('works with negative length', function (): void { + $array = FixedArray::fromArray([1, 2, 3, 4, 5]); + $sliced = FixedArray::slice($array, 1, -2); + + expect($sliced->toArray())->toEqual([2, 3]); + }); + + it('returns an empty array if offset exceeds array length', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $sliced = FixedArray::slice($array, 5); + + expect($sliced->toArray())->toEqual([]); + }); + + it('returns an empty array when slicing an empty SplFixedArray', function (): void { + $array = new SplFixedArray(0); + $sliced = FixedArray::slice($array, 0, 3); + + expect($sliced->count()) + ->toBe(0) + ->and($sliced->toArray()) + ->toEqual([]); + }); +}); + +describe('sort', function (): void { + it('sorts numerically ascending by default', function (): void { + $array = FixedArray::fromArray([3, 1, 4, 2]); + $sorted = FixedArray::sort($array); + + expect(FixedArray::toArray($sorted))->toBe([1, 2, 3, 4]); + }); + + it('sorts using a custom callback', function (): void { + $array = FixedArray::fromArray([3, 1, 4, 2]); + $sorted = FixedArray::sort($array, fn(int $a, int $b): int => $b <=> $a); + + expect(FixedArray::toArray($sorted))->toBe([4, 3, 2, 1]); + }); +}); + +describe('to array', function (): void { + it('converts a non-empty SplFixedArray to a PHP array', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::toArray($array); + + expect($result) + ->toBeArray() + ->toEqual([1, 2, 3]); + }); + + it('converts an empty SplFixedArray to an empty PHP array', function (): void { + $array = new SplFixedArray(0); + $result = FixedArray::toArray($array); + + expect($result) + ->toBeArray() + ->toHaveCount(0); + }); + + it('preserves mixed types', function (): void { + $array = FixedArray::fromArray([1, 'two', null, 4.5, true]); + $result = FixedArray::toArray($array); + + expect($result) + ->toEqual([1, 'two', null, 4.5, true]); + }); +}); + +describe('to collection', function (): void { + it('converts a non-empty SplFixedArray to a collection', function (): void { + $array = FixedArray::fromArray([1, 2, 3]); + $result = FixedArray::toCollection($array); + + expect($result) + ->toBeInstanceOf(Collection::class) + ->toHaveCount(3) + ->toEqual(collect([1, 2, 3])); + }); + + it('converts an empty SplFixedArray to an empty collection', function (): void { + $array = new SplFixedArray(0); + $result = FixedArray::toCollection($array); + + expect($result) + ->toBeInstanceOf(Collection::class) + ->toHaveCount(0) + ->toEqual(collect([])); + }); + + it('preserves mixed types in the collection', function (): void { + $array = FixedArray::fromArray([1, 'two', null, 4.5, true]); + $result = FixedArray::toCollection($array); + + expect($result)->toEqual(collect([1, 'two', null, 4.5, true])); + }); +}); + +describe('unique', function (): void { + it('removes duplicate values using strict comparison', function (): void { + $array = FixedArray::fromArray([1, true, 2, 2, '1']); + $unique = FixedArray::unique($array); + + // true !== 1 and '1' !== 1, so all remain distinct + expect($unique->toArray())->toEqual([1, true, 2, '1']); + }); + + it('removes duplicate values using non-strict comparison', function (): void { + $array = FixedArray::fromArray([1, '1', 2, true]); + $unique = FixedArray::unique($array, false); + + // 1 == '1' == true under non-strict comparison + expect($unique->toArray())->toEqual([1, 2]); + }); + + it('works with mixed types and nulls', function (): void { + $array = FixedArray::fromArray([null, 1, 'foo', null, 'foo', true]); + $unique = FixedArray::unique($array); + + expect($unique->toArray())->toEqual([null, 1, 'foo', true]); + }); + + it('returns empty array if original array has zero indices', function (): void { + $array = new SplFixedArray(0); + $unique = FixedArray::unique($array); + + expect($unique->count()) + ->toBe(0) + ->and($unique->toArray()) + ->toEqual([]); + }); +}); + +describe('unshift', function (): void { + it('prepends a value to a non-empty array', function (): void { + $array = FixedArray::fromArray([2, 3]); + $result = FixedArray::unshift($array, 1); + + expect($array->toArray()) + ->toEqual([1, 2, 3]) + ->and($result) + ->toBe($array); + }); + + it('works on an array with zero indices', function (): void { + $array = new SplFixedArray(0); + $result = FixedArray::unshift($array, 1); + + expect($array->toArray()) + ->toEqual([1]) + ->and($result) + ->toBe($array); + }); + + it('supports mixed types', function (): void { + $array = FixedArray::fromArray(['b', 'c']); + $result = FixedArray::unshift($array, null); + + expect($array->toArray()) + ->toEqual([null, 'b', 'c']) + ->and($result) + ->toBe($array); + }); + + it('preserves original items after prepending', function (): void { + $array = FixedArray::fromArray([true, false]); + FixedArray::unshift($array, 'start'); + + expect($array->toArray())->toEqual(['start', true, false]); + }); +}); diff --git a/tests/Unit/Fluent/FixedArrayableTest.php b/tests/Unit/Fluent/FixedArrayableTest.php new file mode 100644 index 0000000..19935bf --- /dev/null +++ b/tests/Unit/Fluent/FixedArrayableTest.php @@ -0,0 +1,450 @@ +toBeInstanceOf(FixedArrayable::class) + ->and($fluent->get()) + ->toBe($array); + }); + + it('can return itself if given an existing FixedArrayable instance', function (): void { + $original = FixedArrayable::make([1, 2, 3]); + $fluent = FixedArrayable::make($original); + + expect($fluent->toArray())->toBe($original->toArray()); + }); + + it('can create a new default instance if inadvertently given a fixed array object', function (): void { + $array = new class extends FixedArray {}; + $fluent = FixedArrayable::make($array); + + expect($fluent)->toBeInstanceOf(FixedArrayable::class) + ->and($fluent->get()) + ->toBeInstanceOf(SplFixedArray::class); + }); + + it('can wrap an array', function (): void { + $array = [1, 2, 3]; + $fluent = FixedArrayable::make($array); + + expect($fluent->toArray()) + ->toEqual($array) + ->and($fluent->get()) + ->toBeInstanceOf(SplFixedArray::class); + }); + + it('can wrap an Arrayable (e.g., Collection)', function (): void { + $collection = collect([1, 2, 3]); + $fluent = FixedArrayable::make($collection); + + expect($fluent->toArray())->toEqual([1, 2, 3]); + }); + + it('can create a fixed array with a single value', function (): void { + $fluent = FixedArrayable::make(42); + + expect($fluent->toArray())->toEqual([42]); + }); + + it('can create a fixed array of a given count with a single value pushed', function (): void { + $fluent = FixedArrayable::make('x', 5); + + expect($fluent->get()) + ->toBeInstanceOf(SplFixedArray::class) + ->and($fluent->toArray()[0]) + ->toBe('x') + ->and($fluent->toArray()) + ->toHaveCount(5); + }); + + it('use is an alias for make', function (): void { + $fluent1 = FixedArrayable::make([1, 2, 3]); + $fluent2 = FixedArrayable::use([1, 2, 3]); + + expect($fluent2->toArray())->toEqual($fluent1->toArray()); + }); +}); + +describe('value returning methods', function (): void { + beforeEach(function (): void { + $this->contents = [1, 2, 3]; + $this->fluent = FixedArrayable::make($this->contents); + }); + + it('can return the underlying fixed array', function (): void { + expect($this->fluent->get())->toBeInstanceOf(SplFixedArray::class); + }); + + it('can return the underlying fixed array as a standard array', function (): void { + expect($this->fluent->toArray())->toEqual($this->contents); + }); + + it('can return the underlying fixed array as a collection', function (): void { + $collection = $this->fluent->toCollection(); + + expect($collection) + ->toBeInstanceOf(Collection::class) + ->and($collection->toArray()) + ->toEqual($this->contents); + }); + + it('is an alias for get', function (): void { + expect($this->fluent->toFixedArray())->toBe($this->fluent->get()); + }); +}); + +describe('mutator methods', function (): void { + beforeEach(function (): void { + $this->fluent = FixedArrayable::make([1, 2, 3]); + }); + + it('map mutates the fluent instance and returns $this', function (): void { + $returned = $this->fluent->map(fn(int $v): int => $v * 2); + + expect($returned) + ->toBe($this->fluent) + ->and($this->fluent->toArray()) + ->toEqual([2, 4, 6]); + }); + + it('can push (add) values fluently', function (): void { + $returned = $this->fluent->push(4)->push(5); + + expect($returned) + ->toBe($this->fluent) + ->and($this->fluent->toArray()) + ->toEqual([1, 2, 3, 4, 5]); + }); + + it('can addFrom an iterable', function (): void { + $this->fluent->addFrom([4, 5]); + + expect($this->fluent->toArray())->toEqual([1, 2, 3, 4, 5]); + }); + + it('can unshift (prepend) values fluently', function (): void { + $returned = $this->fluent->unshift(0); + + expect($returned) + ->toBe($this->fluent) + ->and($this->fluent->toArray()) + ->toEqual([0, 1, 2, 3]); + }); + + it('can pop and return the last element', function (): void { + $popped = $this->fluent->pop(); + + expect($popped)->toBe(3); + }); + + it('can popToArray appending the popped value into provided array', function (): void { + $output = []; + $returned = $this->fluent->popToArray($output); + + expect($returned) + ->toBe($this->fluent) + ->and($output) + ->toEqual([3]); + }); + + it('can shift and return the first element', function (): void { + $shifted = $this->fluent->shift(); + + // after shift, first() should now be the next element + expect($shifted) + ->toBe(1) + ->and($this->fluent->first()) + ->toBe(2); + }); + + it('can setSize (resize) the array', function (): void { + $returned = $this->fluent->setSize(5); + + expect($returned) + ->toBe($this->fluent) + ->and($this->fluent->getSize()) + ->toBe(5); + }); + + it('can merge with an iterable (array) source', function (): void { + $returned = $this->fluent->merge([4, 5]); + + expect($returned) + ->toBe($this->fluent) + ->and($this->fluent->toArray()) + ->toEqual([1, 2, 3, 4, 5]); + }); + + it('can nullify values', function (): void { + $returned = $this->fluent->nullify(); + + expect($returned)->toBe($this->fluent); + $arr = $this->fluent->toArray(); + + foreach ($arr as $v) { + expect($v)->toBeNull(); + } + }); + + it('can make values unique', function (): void { + $f = FixedArrayable::make([1, 2, 2, 3]); + $f->unique(); + + expect($f->toArray())->toEqual([1, 2, 3]); + }); +}); + +describe('query methods', function (): void { + beforeEach(function (): void { + $this->fluent = FixedArrayable::make([10, 20, 30]); + }); + + it('can determine if a value exists', function (): void { + expect($this->fluent->contains(20)) + ->toBeTrue() + ->and($this->fluent->contains(99)) + ->toBeFalse(); + }); + + it('can return the size via getSize', function (): void { + expect($this->fluent->getSize())->toBe(3); + }); + + it('can be checked empty by getSize()', function (): void { + expect(FixedArrayable::make([]) + ->getSize()) + ->toBe(0) + ->and($this->fluent->getSize()) + ->toBeGreaterThan(0); + }); +}); + +describe('conversion & other sanity checks', function (): void { + beforeEach(function (): void { + $this->fluent = FixedArrayable::make([1, 2, 3]); + }); + + it('toArray and toCollection behave', function (): void { + expect($this->fluent->toArray())->toEqual([1, 2, 3]); + + $collection = $this->fluent->toCollection(); + + expect($collection) + ->toBeInstanceOf(Collection::class) + ->and($collection->toArray()) + ->toEqual([1, 2, 3]); + }); + + it('toFixedArray is alias for get', function (): void { + expect($this->fluent->toFixedArray())->toBe($this->fluent->get()); + }); + + it('merge accepts SplFixedArray', function (): void { + $this->fluent->setSize(3); + $this->fluent->merge(SplFixedArray::fromArray([4, 5])); + + expect($this->fluent->toArray())->toEqual([1, 2, 3, 4, 5]); + }); + + it('random returns one of the existing values or null if empty', function (): void { + $val = $this->fluent->random(); + + // should be one of 1, 2, 3 (or null if empty, but array is not empty!) + expect(in_array($val, [1, 2, 3], true))->toBeTrue(); + }); +}); + +describe('fluency', function (): void { + beforeEach(function (): void { + $this->fluent = FixedArrayable::make([1, 2, 3, 4, 5]); + }); + + it('can return a nonsense array', function (): void { + $this->fluent->dsfargeg(); + + expect($this->fluent->toArray()) + ->not()->toEqual([1, 2, 3, 4, 5]) + ->toEqual(['D', 'S', 'F', 'A', 'R', 'G', 'E', 'G']); + }); + + it('ensures chainable methods return FixedArrayable and chain works', function (): void { + $fluent = FixedArrayable::make([1, 2, 3]) + ->map(fn(int $v): int => $v) + ->push(4) + ->unshift(0) + ->merge([5]); + + expect($fluent) + ->toBeInstanceOf(FixedArrayable::class) + ->and($fluent->toArray())->toEqual([0, 1, 2, 3, 4, 5]); + }); + + describe('add', function (): void { + it('can add a single item and remain fluent', function (): void { + $result = $this->fluent->add(6); + + expect($result) + ->toBeInstanceOf(FixedArrayable::class) + ->and($result->toArray())->toEqual([1, 2, 3, 4, 5, 6]); + }); + }); + + describe('chunk', function (): void { + it('can split into smaller fixed arrays', function (): void { + $chunks = $this->fluent->chunk(2); + + expect($chunks) + ->toBeInstanceOf(FixedArrayable::class) + ->and($chunks->toArray()) + ->toHaveCount(3); + }); + }); + + describe('chunkWhile', function (): void { + it('can chunk while a condition holds', function (): void { + $chunks = $this->fluent->chunkWhile(fn(int $v, int $k): bool => $v < 4); + + expect($chunks) + ->toBeInstanceOf(FixedArrayable::class) + ->and($chunks->toArray()[0]->toArray())->toEqual([1, 2, 3]); + }); + }); + + describe('each', function (): void { + it('can iterate through each element fluently', function (): void { + $sum = 0; + $result = $this->fluent->each(function (int $v) use (&$sum): void { + $sum += $v; + }); + + expect($sum) + ->toBe(15) + ->and($result) + ->toBeInstanceOf(FixedArrayable::class); + }); + }); + + describe('fill', function (): void { + it('can fill a fixed array with a single value', function (): void { + $filled = $this->fluent->fill('x'); + + expect($filled->toArray())->toEqual(['x', 'x', 'x', 'x', 'x']); + }); + }); + + describe('find', function (): void { + it('can find a value matching a condition', function (): void { + $found = $this->fluent->find(fn(int $v): bool => $v > 3); + expect($found)->toBe(4); + }); + }); + + describe('findKey', function (): void { + it('can find the key for a matching value', function (): void { + $key = $this->fluent->findKey(fn(int $v): bool => $v === 3); + expect($key)->toBe(2); + }); + }); + + describe('findIndex', function (): void { + it('can find the numeric index for a matching value', function (): void { + $index = $this->fluent->findIndex(fn(int $v): bool => $v === 2); + expect($index)->toBe(1); + }); + }); + + describe('flatten', function (): void { + it('can flatten nested fixed arrays', function (): void { + $nested = FixedArrayable::make([[1, 2], [3, 4]]); + $flattened = $nested->flatten(); + + expect($flattened->toArray())->toEqual([1, 2, 3, 4]); + }); + }); + + describe('last', function (): void { + it('can retrieve the last value', function (): void { + expect($this->fluent->last())->toBe(5); + }); + }); + + describe('offsetExists', function (): void { + it('can check if an index exists', function (): void { + expect($this->fluent->offsetExists(2)) + ->toBeTrue() + ->and($this->fluent->offsetExists(10)) + ->toBeFalse(); + }); + }); + + describe('offsetGet', function (): void { + it('can retrieve a value by offset', function (): void { + expect($this->fluent->offsetGet(1))->toBe(2); + }); + }); + + describe('offsetSet', function (): void { + it('can set a value by offset', function (): void { + $this->fluent->offsetSet(1, 42); + expect($this->fluent->toArray()[1])->toBe(42); + }); + }); + + describe('resize', function (): void { + it('can increase or decrease size', function (): void { + $resized = $this->fluent->resize(3); + expect($resized->toArray())->toEqual([1, 2, 3]); + }); + }); + + describe('reverse', function (): void { + it('can reverse the order of elements', function (): void { + $reversed = $this->fluent->reverse(); + + expect($reversed->toArray())->toEqual([5, 4, 3, 2, 1]); + }); + }); + + describe('second', function (): void { + it('can return the second value', function (): void { + expect($this->fluent->second())->toBe(2); + }); + }); + + describe('shuffle', function (): void { + it('can shuffle the order randomly', function (): void { + $shuffled = $this->fluent->shuffle(); + + expect($shuffled) + ->toBeInstanceOf(FixedArrayable::class) + ->and($shuffled->toArray())->toHaveCount(5); + }); + }); + + describe('slice', function (): void { + it('can slice a segment from the array', function (): void { + $sliced = $this->fluent->slice(1, 3); + + expect($sliced->toArray())->toEqual([2, 3, 4]); + }); + }); + + describe('sort', function (): void { + it('can sort the fixed array', function (): void { + $unsorted = FixedArrayable::make([3, 1, 2]); + $sorted = $unsorted->sort(); + + expect($sorted->toArray())->toEqual([1, 2, 3]); + }); + }); +}); diff --git a/tests/Unit/HelperTest.php b/tests/Unit/HelperTest.php new file mode 100644 index 0000000..fceaca1 --- /dev/null +++ b/tests/Unit/HelperTest.php @@ -0,0 +1,17 @@ +toBeInstanceOf(FixedArrayable::class) + ->and($fixedArray->toArray())->toEqual([1, 2, 3]); +}); diff --git a/tests/Unit/Providers/FixedArrayServiceProviderTest.php b/tests/Unit/Providers/FixedArrayServiceProviderTest.php new file mode 100644 index 0000000..c1a0dfd --- /dev/null +++ b/tests/Unit/Providers/FixedArrayServiceProviderTest.php @@ -0,0 +1,20 @@ +toBeInstanceOf(FixedArrayServiceProvider::class); + + $package = new Package(); + $provider->configurePackage($package); + + expect($package->name)->toEqual('fixed-array-functions'); +});