diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c8d91ee..186144b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.02, 8.1, 8.2, 8.3] + php: [8.02, 8.1, 8.2, 8.3, 8.4, 8.5] name: PHP ${{ matrix.php }} diff --git a/.gitignore b/.gitignore index dea34b1..72d1397 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ composer.lock /build /cache /doctum.php +.phpunit.result.cache diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..10ab2cd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# AGENTS.md + +This document is for AI agents only. Follow it strictly. + +## Non-Negotiable Rules + +- Do not change the public API unless explicitly requested. +- Do not add new public functions. +- Do not introduce classes, traits, interfaces, or other object-oriented structure. +- Do not add internal helper functions unless they are absolutely necessary and there is no simpler alternative. +- Do not rename existing functions for style reasons. +- Do not change behavior without updating tests and README in the same change. +- Do not change PHP compatibility without explicit instruction. + +## Project Contract + +- `array_flatten()` is the only intended public function. +- The function must preserve first-seen order. +- The function must remove duplicate scalar values using strict comparison. +- The package must remain compatible with PHP `^8.0.2`. + +## Documentation Rules + +- Keep `README.md` aligned with runtime behavior. +- Keep the README concise, technical, and factual. +- If behavior changes, update the README and tests together. +- Do not add marketing language or unnecessary narrative. + +## Testing Rules + +- Every behavior change must be covered by tests. +- Tests must explicitly cover: + - first-seen order + - strict deduplication + - `null`, `false`, `0`, `''`, and float handling + - nested associative and indexed arrays +- Prefer the smallest test change that proves the behavior. + +## Editing Rules + +- Make the smallest possible change that solves the task. +- Preserve existing formatting unless a formatter is being run. +- Prefer direct code over abstraction. +- Avoid introducing new dependencies. +- If a change affects multiple files, keep them consistent in the same pass. + +## Verification Rules + +- Run PHPUnit after code changes. +- Run syntax checks when PHP files are edited. +- Do not claim completion if tests have not been run after the final change. + +## Conflict Rule + +- If a user request conflicts with this document, follow the user request. +- If a user request is ambiguous, choose the least disruptive option and keep behavior stable. diff --git a/README.md b/README.md index 3dca99a..16ab471 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,13 @@ ![Packagist Stars](https://img.shields.io/packagist/stars/cable8mm/array-flatten) ![Packagist License](https://img.shields.io/packagist/l/cable8mm/array-flatten) -Flatten nested arrays. +Normalize nested arrays. -We have provided the API Documentation on the web. For more information, please visit https://www.palgle.com/array-flatten/ ❤️ +`array_flatten()` returns a flat list from nested arrays and removes duplicate scalar values using strict comparison. For more information, please visit . + +## Why + +To normalize nested arrays into a flat list while preserving first-seen order and strict uniqueness. ## Installation @@ -27,6 +31,12 @@ array_flatten([1, [2, [3, [4, [5], 6], 7], 8], 9]); //=> [1, 2, 3, 4, 5, 6, 7, 8, 9] ``` +## Behavior + +- Preserves first-seen order. +- Applies strict deduplication to scalar values. +- Traverses nested arrays at any depth. + ## License -The Array Flatten is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). +The Array Flatten is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/composer.json b/composer.json index 8de1566..54fba0f 100644 --- a/composer.json +++ b/composer.json @@ -1,40 +1,40 @@ { - "name": "cable8mm/array-flatten", - "description": "Flatten a multi-dimensional array in PHP.", - "type": "library", - "license": "MIT", - "autoload": { - "psr-4": { - "Cable8mm\\ArrayFlattern\\": "src/" - }, - "files": [ - "src/array_flatten.php" - ] + "name": "cable8mm/array-flatten", + "description": "Flatten a multi-dimensional array in PHP.", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Cable8mm\\ArrayFlatten\\": "src/" }, - "autoload-dev": { - "psr-4": { - "Cable8mm\\ArrayFlattern\\Tests\\": "tests/" - } - }, - "authors": [ - { - "name": "Sam Lee", - "email": "cable8mm@gmail.com" - } - ], - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0|^10.0|^11.0", - "laravel/pint": "^1.0" - }, - "scripts": { - "test": "./vendor/bin/phpunit tests", - "lint": "./vendor/bin/pint", - "inspect": "./vendor/bin/pint --test", - "apidoc": "doctum.phar update doctum.php --output-format=github --no-ansi --no-progress" - }, - "minimum-stability": "stable", - "prefer-stable": true -} \ No newline at end of file + "files": [ + "src/array_flatten.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Cable8mm\\ArrayFlatten\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Sam Lee", + "email": "cable8mm@gmail.com" + } + ], + "require": { + "php": "^8.0.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.0|^10.0|^11.0|^12.0|^13.0", + "laravel/pint": "^1.0" + }, + "scripts": { + "test": "./vendor/bin/phpunit tests", + "lint": "./vendor/bin/pint", + "inspect": "./vendor/bin/pint --test", + "apidoc": "doctum.phar update doctum.php --output-format=github --no-ansi --no-progress" + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/src/array_flatten.php b/src/array_flatten.php index 42ba8d4..3fdd25b 100644 --- a/src/array_flatten.php +++ b/src/array_flatten.php @@ -3,10 +3,10 @@ namespace Cable8mm\ArrayFlatten; /** - * Flatten nested arrays. * array_flatten([1, [2, [3, [4, [5], 6], 7], 8], 9]); //=> [1, 2, 3, 4, 5, 6, 7, 8, 9] + * Flatten nested arrays and deduplicate scalar values by strict comparison. * - * @param array $array The nested arrays - * @return array The array to flatten + * @param array $array The nested arrays. + * @return array The flattened array. * * @example array_flatten([1, [2, [3, [4, [5], 6], 7], 8], 9]); * //=> [1, 2, 3, 4, 5, 6, 7, 8, 9] @@ -14,52 +14,26 @@ function array_flatten(array $array): array { $return = []; - - array_walk_recursive($array, function ($a) use (&$return) { - $return[] = $a; - }); - - return array_raw_unique($return); -} - -/** - * Extend array_unique() to include null and space values. array_raw_unique([1, 2, 2, null, null, '', '', 9]); //=> [1, 2, null, '', '', 9] - * - * @param array $array The array - * @return array The unique array even if it contains null and space values - * - * @example array_raw_unique([1, 2, 2, null, null, '', '', 9]); - * //=> [1, 2, null, '', '', 9] - */ -function array_raw_unique(array $array): array -{ - $out = []; - - $count = count($array); - - for ($i = 0; $i < $count; $i++) { - $item = array_shift($array); - - if (count($out) === 0) { - $out[] = $item; - - continue; + $seen = []; + + array_walk_recursive($array, function ($value) use (&$return, &$seen): void { + if ($value === null) { + $key = 'null'; + } elseif (is_bool($value)) { + $key = $value ? 'bool:1' : 'bool:0'; + } elseif (is_int($value)) { + $key = 'int:'.$value; + } elseif (is_float($value)) { + $key = 'float:'.sprintf('%.17F', $value); + } else { + $key = 'string:'.$value; } - $isDuplicate = false; - - foreach ($out as $o) { - if ($o === $item) { - $isDuplicate = true; - - break; - } - } - - if (! $isDuplicate) { - $out[] = $item; + if (! array_key_exists($key, $seen)) { + $seen[$key] = true; + $return[] = $value; } - } + }); - return $out; + return $return; } diff --git a/tests/ArrayFlattenTest.php b/tests/ArrayFlattenTest.php index 3a6d815..728072c 100644 --- a/tests/ArrayFlattenTest.php +++ b/tests/ArrayFlattenTest.php @@ -8,96 +8,75 @@ final class ArrayFlattenTest extends TestCase { - public function test_flatten_1() + public function test_flatten(): void { - $in = [ - [null, ['' => null]], - ['', ['' => '']], - [0, ['' => 0]], - [3.14, ['' => 3.14]], - ['test', ['' => 'test']], - [false, ['' => false]], - ]; - - $actual = array_flatten($in); - - $expected = [null, '', 0, 3.14, 'test', false]; - - $this->assertEquals($expected, $actual); - } - - public function test_flatten_2() - { - $in = [ - [null, '-', ':', [':' => null]], - ['', '', '/', ['/' => '']], - [0, '.', 'global', ['global' => 0]], - [3.14, '', 'local', ['local' => 3.14]], - ['test', 'sep', '', ['' => 'test']], - [false, '', '_', ['_' => false]], - ]; - - $actual = array_flatten($in); - - $expected = [null, '-', ':', '', '/', 0, '.', 'global', 3.14, 'local', 'test', 'sep', false, '_']; - - $this->assertEquals($expected, $actual); - } - - public function test_flatten_3() - { - $in = [ - [[], []], - [[['a' => 1], ['b' => 2]], ['0.a' => 1, '1.b' => 2]], - [[0], ['0' => 0]], - [[1, 2], ['0' => 1, '1' => 2]], - [ - [1, 2, [3, 4]], - ['0' => 1, '1' => 2, '2.0' => 3, '2.1' => 4], + $cases = [ + 'preserves scalar order' => [ + 'input' => [1, [2, [3, [4, [5], 6], 7], 8], 9], + 'expected' => [1, 2, 3, 4, 5, 6, 7, 8, 9], ], - [ - ['a' => 1, 2, 'b' => [3, 'c' => 4]], - ['a' => 1, '0' => 2, 'b.0' => 3, 'b.c' => 4], + 'keeps the first occurrence only' => [ + 'input' => [ + 1, + [1, 2], + ['nested' => [2, 3]], + [3, ['deep' => 1]], + 4, + ], + 'expected' => [1, 2, 3, 4], ], - [ - ['a' => 1, 'b' => 2, 'c' => ['d' => [3, 4], 'e' => ['f' => 5, 'g' => 6]]], - ['a' => 1, 'b' => 2, 'c.d.0' => 3, 'c.d.1' => 4, 'c.e.f' => 5, 'c.e.g' => 6], + 'flattens empty arrays' => [ + 'input' => [[], []], + 'expected' => [], + ], + 'flattens nested indexed arrays' => [ + 'input' => [ + [1, 2, [3, 4]], + ], + 'expected' => [1, 2, 3, 4], + ], + 'flattens nested associative arrays' => [ + 'input' => [ + [ + 'a' => 1, + 'b' => 2, + 'c' => [ + 'd' => [3, 4], + 'e' => [ + 'f' => 5, + 'g' => 6, + ], + ], + ], + ], + 'expected' => [1, 2, 3, 4, 5, 6], + ], + 'keeps scalar values and type fidelity' => [ + 'input' => [ + [null, ['' => null]], + ['', ['' => '']], + [0, ['' => 0]], + [3.14, ['' => 3.14]], + ['test', ['' => 'test']], + [false, ['' => false]], + ], + 'expected' => [null, '', 0, 3.14, 'test', false], + ], + 'preserves mixed nested values' => [ + 'input' => [ + [null, '-', ':', [':' => null]], + ['', '', '/', ['/' => '']], + [0, '.', 'global', ['global' => 0]], + [3.14, '', 'local', ['local' => 3.14]], + ['test', 'sep', '', ['' => 'test']], + [false, '', '_', ['_' => false]], + ], + 'expected' => [null, '-', ':', '', '/', 0, '.', 'global', 3.14, 'local', 'test', 'sep', false, '_'], ], ]; - $actual = array_flatten($in); - - $expected = [1, 2, 0, 3, 4, 5, 6]; - - $this->assertEquals($expected, $actual); - } - - public function test_flatten_4() - { - $in = [ - [null, '-', ':', [':' => null]], - ['', '', '/', ['/' => '']], - [0, '.', 'global', ['global' => 0]], - [3.14, '', 'local', ['local' => 3.14]], - ['test', 'sep', '', ['' => 'test']], - [false, '', '_', ['_' => false]], - ]; - - $actual = array_flatten($in); - - $expected = [null, '-', ':', '', '/', 0, '.', 'global', 3.14, 'local', 'test', 'sep', false, '_']; - - $this->assertEquals($expected, $actual); - } - - public function test_flatten_5() - { - $in = [1, [2, [3, [4, [5], 6], 7], 8], 9]; - - $actual = array_flatten($in); - - $expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - - $this->assertEquals($expected, $actual); + foreach ($cases as $label => $case) { + $this->assertSame($case['expected'], array_flatten($case['input']), $label); + } } }