├── docs ├── Gemfile ├── index.md └── _config.yml ├── .github ├── benchmarks.png ├── logo-black.png ├── logo-white.png └── workflows │ ├── linting.yml │ └── ci.yml ├── .gitignore ├── tests ├── bootstrap.php ├── Helpers │ ├── MockIteratorAggregate.php │ ├── Generators.php │ └── ArrayAccessible.php └── __ │ ├── Arrays │ ├── PrependTest.php │ ├── RepeatTest.php │ ├── AppendTest.php │ ├── RangeTest.php │ ├── PatchTest.php │ ├── RandomizeTest.php │ ├── CompactTest.php │ ├── DropTest.php │ ├── DropRightTest.php │ ├── DropRightWhileTest.php │ └── DropWhileTest.php │ ├── Utilities │ ├── NowTest.php │ └── IdentityTest.php │ ├── Objects │ ├── IsNullTest.php │ ├── IsArrayTest.php │ ├── IsNumberTest.php │ ├── IsStringTest.php │ ├── IsEmailTest.php │ ├── IsObjectTest.php │ ├── IsFunctionTest.php │ └── IsCollectionTest.php │ ├── Strings │ ├── CapitalizeTest.php │ ├── LowerFirstTest.php │ ├── UpperFirstTest.php │ ├── SplitTest.php │ ├── KebabCaseTest.php │ ├── SnakeCaseTest.php │ ├── ToLowerTest.php │ ├── ToUpperTest.php │ ├── CamelCaseTest.php │ ├── LowerCaseTest.php │ ├── StartCaseTest.php │ ├── UpperCaseTest.php │ └── WordsTest.php │ ├── Functions │ ├── TruncateTest.php │ ├── SlugTest.php │ └── UrlifyTest.php │ ├── Collections │ ├── MaxTest.php │ ├── MinTest.php │ ├── UneaseTest.php │ ├── FirstTest.php │ ├── ReduceRightTest.php │ ├── DoForEachRightTest.php │ ├── HasTest.php │ ├── AssignTest.php │ ├── ReverseIterableTest.php │ ├── HasKeysTest.php │ ├── FindTest.php │ ├── FindLastTest.php │ ├── WhereTest.php │ ├── SomeTest.php │ ├── FindLastIndexTest.php │ ├── LastTest.php │ ├── EveryTest.php │ ├── DoForEachTest.php │ ├── EaseTest.php │ ├── PickTest.php │ ├── PluckTest.php │ ├── SizeTest.php │ ├── IsEmptyTest.php │ ├── MergeTest.php │ ├── MapTest.php │ ├── SetTest.php │ ├── GetTest.php │ ├── FindIndexTest.php │ └── ConcatDeepTest.php │ └── Sequences │ └── ChainTest.php ├── src └── __ │ ├── utilities │ ├── now.php │ └── identity.php │ ├── objects │ ├── isNull.php │ ├── isArray.php │ ├── isObject.php │ ├── isString.php │ ├── isFunction.php │ ├── isNumber.php │ ├── isEmail.php │ ├── isCollection.php │ ├── isIterable.php │ └── isEqual.php │ ├── strings │ ├── toUpper.php │ ├── lowerFirst.php │ ├── toLower.php │ ├── upperFirst.php │ ├── capitalize.php │ ├── lowerCase.php │ ├── upperCase.php │ ├── snakeCase.php │ ├── kebabCase.php │ ├── camelCase.php │ ├── startCase.php │ └── split.php │ ├── arrays │ ├── prepend.php │ ├── append.php │ ├── randomize.php │ ├── repeat.php │ ├── range.php │ ├── compact.php │ ├── patch.php │ ├── flatten.php │ ├── drop.php │ ├── dropRight.php │ ├── chunk.php │ ├── dropWhile.php │ └── dropRightWhile.php │ ├── sequences │ └── chain.php │ ├── collections │ ├── min.php │ ├── max.php │ ├── getIterator.php │ ├── isEmpty.php │ ├── first.php │ ├── doForEachRight.php │ ├── reverseIterable.php │ ├── doForEach.php │ ├── last.php │ ├── unease.php │ ├── hasKeys.php │ ├── size.php │ ├── where.php │ ├── map.php │ ├── reduceRight.php │ ├── pick.php │ ├── mapValues.php │ ├── merge.php │ ├── every.php │ ├── assign.php │ ├── ease.php │ ├── some.php │ ├── has.php │ ├── pluck.php │ ├── mapKeys.php │ ├── filter.php │ ├── findLastEntry.php │ ├── find.php │ ├── concat.php │ ├── get.php │ ├── concatDeep.php │ ├── findLast.php │ ├── findEntry.php │ ├── findIndex.php │ ├── findLastIndex.php │ ├── reduce.php │ └── set.php │ └── functions │ ├── truncate.php │ └── urlify.php ├── vendor-bin └── php-cs-fixer │ └── composer.json ├── bottomline.php ├── .editorconfig ├── docgen ├── Parsers.php └── ArgumentDocumentation.php ├── phpunit.xml ├── LICENSE ├── .php-cs-fixer.dist.php ├── README.md ├── composer.json └── CONTRIBUTING.md /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'github-pages', group: :jekyll_plugins 4 | -------------------------------------------------------------------------------- /.github/benchmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/benchmarks.png -------------------------------------------------------------------------------- /.github/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/logo-black.png -------------------------------------------------------------------------------- /.github/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/logo-white.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .php_cs.cache 3 | .php-cs-fixer.cache 4 | .phpunit.result.cache 5 | _site/ 6 | composer.phar 7 | Gemfile.lock 8 | vendor/ 9 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | data = $data; 12 | } 13 | 14 | #[\ReturnTypeWillChange] 15 | public function getIterator() 16 | { 17 | return new \ArrayIterator($this->data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/__/Arrays/PrependTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([4, 1, 2, 3], $x); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/__/Utilities/NowTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, is_numeric($x)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/__/Arrays/RepeatTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([$string, $string, $string], $x); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__/strings/toUpper.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__/utilities/identity.php: -------------------------------------------------------------------------------- 1 | $value) { 17 | yield $key => $value; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/__/Objects/IsArrayTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Objects/IsNumberTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Objects/IsStringTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Objects/IsEmailTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Objects/IsObjectTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(false, $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Strings/CapitalizeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Fred', $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__/arrays/prepend.php: -------------------------------------------------------------------------------- 1 | assertEquals([1, 2, 3, 4], $x); 19 | $this->assertEquals([1, 2, 3, [4, 5]], $x2); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__/arrays/append.php: -------------------------------------------------------------------------------- 1 | assertEquals([1, 2, 3, 4, 5], $x); 19 | $this->assertEquals([-2, -1, 0, 1, 2], $y); 20 | $this->assertEquals([1, 3, 5, 7, 9], $z); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/__/Objects/IsFunctionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/__/Utilities/IdentityTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('arg 1', $x); 19 | 20 | // Act 21 | $x = __::identity(); 22 | 23 | // Assert 24 | $this->assertEquals(null, $x); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/__/Functions/TruncateTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Lorem ipsum dolor sit amet, ...', $x); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__/Strings/LowerFirstTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('fred', $x); 24 | $this->assertEquals('fRED', $y); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/__/Strings/UpperFirstTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Fred', $x); 24 | $this->assertEquals('FRED', $y); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/__/Arrays/PatchTest.php: -------------------------------------------------------------------------------- 1 | ['country' => 'US', 'tel' => [123]], 99]; 14 | $p = ['/0' => 2, '/1' => 3, '/contacts/country' => 'CA', '/contacts/tel/0' => 3456]; 15 | 16 | // Act 17 | $x = __::patch($a, $p); 18 | 19 | // Assert 20 | $this->assertEquals([2, 3, 1, 'contacts' => ['country' => 'CA', 'tel' => [3456]], 99], $x); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/__/arrays/randomize.php: -------------------------------------------------------------------------------- 1 | compact() 16 | * ->prepend(4) 17 | * ->value() 18 | * ; 19 | * ``` 20 | * 21 | * **Result** 22 | * 23 | * ``` 24 | * [4, 1, 2, 3] 25 | * ``` 26 | * 27 | * @param mixed $initialValue 28 | * 29 | * @return \BottomlineWrapper 30 | */ 31 | function chain($initialValue) 32 | { 33 | return new \BottomlineWrapper($initialValue); 34 | } 35 | -------------------------------------------------------------------------------- /src/__/arrays/range.php: -------------------------------------------------------------------------------- 1 | assertEquals(['github', 'com'], $x); 25 | $this->assertEquals(['a', 'b', 'c'], $y); 26 | $this->assertEquals(['a', 'b-c'], $z); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/__/Functions/SlugTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('jakies-zdanie-z-duza-iloscia-obcych-znakow', $actual); 18 | } 19 | 20 | public function testSlugWithAscii() 21 | { 22 | $input = 'Hello World!'; 23 | $actual = __::slug($input); 24 | 25 | $this->assertEquals('hello-world', $actual); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/__/Strings/KebabCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foo-bar', $x); 26 | $this->assertEquals('foo-bar', $y); 27 | $this->assertEquals('foo-bar', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/SnakeCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foo_bar', $x); 26 | $this->assertEquals('foo_bar', $y); 27 | $this->assertEquals('foo_bar', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/ToLowerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('--foo-bar--', $x); 26 | $this->assertEquals('foobar', $y); 27 | $this->assertEquals('__foo_bar__', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/ToUpperTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('--FOO-BAR--', $x); 26 | $this->assertEquals('FOOBAR', $y); 27 | $this->assertEquals('__FOO_BAR__', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/CamelCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('fooBar', $x); 26 | $this->assertEquals('fooBar', $y); 27 | $this->assertEquals('fooBar', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/LowerCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foo bar', $x); 26 | $this->assertEquals('foo bar', $y); 27 | $this->assertEquals('foo bar', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/StartCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Foo Bar', $x); 26 | $this->assertEquals('Foo Bar', $y); 27 | $this->assertEquals('FOO BAR', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Strings/UpperCaseTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('FOO BAR', $x); 26 | $this->assertEquals('FOO BAR', $y); 27 | $this->assertEquals('FOO BAR', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Collections/MaxTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(3, $x); 22 | } 23 | 24 | public function testMaxIterable() 25 | { 26 | // Arrange 27 | $a = new ArrayIterator([1, 2, 3]); 28 | 29 | // Act 30 | $x = __::max($a); 31 | 32 | // Assert 33 | $this->assertEquals(3, $x); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/__/Collections/MinTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, $x); 22 | } 23 | 24 | public function testMinIterable() 25 | { 26 | // Arrange 27 | $a = new ArrayIterator([1, 2, 3]); 28 | 29 | // Act 30 | $x = __::min($a); 31 | 32 | // Assert 33 | $this->assertEquals(1, $x); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/__/Objects/IsCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(true, $x); 26 | $this->assertEquals(true, $y); 27 | $this->assertEquals(false, $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/__/Arrays/RandomizeTest.php: -------------------------------------------------------------------------------- 1 | assertNotEquals([1, 2, 3, 4], $x); 26 | $this->assertEquals([1], $y); 27 | $this->assertEquals([2, 1], $z); 28 | $this->assertEquals([], $f); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/__/collections/min.php: -------------------------------------------------------------------------------- 1 | $maxValue) { 32 | $maxValue = $value; 33 | } 34 | } 35 | return $maxValue; 36 | } 37 | -------------------------------------------------------------------------------- /src/__/strings/lowerCase.php: -------------------------------------------------------------------------------- 1 | content); 13 | } 14 | 15 | #[\ReturnTypeWillChange] 16 | public function offsetGet($offset) 17 | { 18 | return $this->content[$offset]; 19 | } 20 | 21 | #[\ReturnTypeWillChange] 22 | public function offsetSet($offset, $value) 23 | { 24 | $this->content[$offset] = $value; 25 | } 26 | 27 | #[\ReturnTypeWillChange] 28 | public function offsetUnset($offset) 29 | { 30 | unset($this->content[$offset]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/__/strings/snakeCase.php: -------------------------------------------------------------------------------- 1 | create(ParserFactory::PREFER_PHP5); 25 | } 26 | if (!isset(self::$markdown)) { 27 | self::$markdown = new Parsedown(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/__/strings/camelCase.php: -------------------------------------------------------------------------------- 1 | $limit) { 30 | $words = str_word_count($text, 2); 31 | $pos = array_keys($words); 32 | $text = mb_substr($text, 0, $pos[$limit], 'UTF-8') . '...'; 33 | } 34 | 35 | return $text; 36 | } 37 | -------------------------------------------------------------------------------- /src/__/strings/startCase.php: -------------------------------------------------------------------------------- 1 | assertEquals('I love google.com', $x); 26 | $this->assertEquals('I love google.com', $y); 27 | $this->assertEquals('I love google.com !', $z); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/__/collections/getIterator.php: -------------------------------------------------------------------------------- 1 | getIterator(); 25 | } 26 | 27 | throw new \InvalidArgumentException('$input should implement the Iterator or IteratorAggregate interface'); 28 | } 29 | -------------------------------------------------------------------------------- /src/__/collections/isEmpty.php: -------------------------------------------------------------------------------- 1 | assertEquals(['fred', 'barney', 'pebbles'], $x); 29 | $this->assertEquals(['fred', 'barney', '&', 'pebbles'], $y); 30 | $this->assertEquals([], $z); 31 | $this->assertEquals(['foo', 'Bar'], $u); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/__/strings/split.php: -------------------------------------------------------------------------------- 1 | 10 |
11 | 12 | ## Installation 13 | 14 | Install bottomline in your project via [Composer](https://getcomposer.org/). 15 | 16 | ```bash 17 | composer require maciejczyzewski/bottomline 18 | ``` 19 | 20 | ### Requirements 21 | 22 | - PHP 5.5+ 23 | 24 |
25 |
26 | 27 | ## Documentation 28 | 29 | This library organizes functions into several namespaces based on their own functionality. These are the available namespaces: 30 | 31 | {% assign namespaces = site.data.fxn_registry.methods | map: "namespace" | uniq %} 32 | {% for method in namespaces -%} 33 | - [{{ method | capitalize }}](documentation/#{{ method }}) 34 | {% endfor %} 35 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /src/__/collections/first.php: -------------------------------------------------------------------------------- 1 | 'ter', 'baz.0' => 'b', 'baz.1' => 'z']; 15 | 16 | // Act 17 | $x = __::unease($a); 18 | 19 | // Assert 20 | $this->assertCount(2, $x); 21 | $this->assertEquals(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']], $x); 22 | } 23 | 24 | public function testUneaseIterable() 25 | { 26 | // Arrange 27 | $a = new ArrayIterator(['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z']); 28 | 29 | // Act 30 | $x = __::unease($a); 31 | 32 | // Assert 33 | $this->assertCount(2, $x); 34 | $this->assertEquals(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']], $x); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/__/Collections/FirstTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, $x); 22 | $this->assertEquals([1, 2], $y); 23 | } 24 | 25 | public function testFirstIterable() 26 | { 27 | if (version_compare(PHP_VERSION, '7.1', '<')) { 28 | return; 29 | } 30 | // Arrange 31 | $a = new ArrayIterator([1, 2, 3, 4, 5]); 32 | 33 | // Act 34 | $x = __::first($a); 35 | $y = __::first($a, 2); 36 | 37 | // Assert 38 | $this->assertEquals(1, $x); 39 | $this->assertEquals([1, 2], $y); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/__/collections/doForEachRight.php: -------------------------------------------------------------------------------- 1 | current($iterable_values); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/__/collections/doForEach.php: -------------------------------------------------------------------------------- 1 | $value) { 33 | if ($iteratee($value, $key, $collection) === false) { 34 | break; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/__/collections/last.php: -------------------------------------------------------------------------------- 1 | = $take) { 40 | return false; 41 | } 42 | 43 | $result = \__::prepend($result, $value); 44 | }); 45 | 46 | return $result; 47 | } 48 | -------------------------------------------------------------------------------- /src/__/collections/unease.php: -------------------------------------------------------------------------------- 1 | 'ter', 'baz.0' => 'b', , 'baz.1' => 'z']); 13 | * ``` 14 | * 15 | * **Result** 16 | * 17 | * ``` 18 | * ['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']] 19 | * ``` 20 | * 21 | * @since 0.2.0 added support for iterables 22 | * 23 | * @param iterable|\stdClass $collection Hash map of values 24 | * @param string $separator The glue used in the keys 25 | * 26 | * @return array 27 | */ 28 | function unease($collection, $separator = '.') 29 | { 30 | $nonDefaultSeparator = $separator !== '.'; 31 | $map = []; 32 | foreach ($collection as $key => $value) { 33 | $map = \__::set( 34 | $map, 35 | $nonDefaultSeparator ? str_replace($separator, '.', $key) : $key, 36 | $value 37 | ); 38 | } 39 | 40 | return $map; 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | tests 27 | vendor 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/__/collections/hasKeys.php: -------------------------------------------------------------------------------- 1 | 'bar', 'foz' => 'baz'], ['foo', 'foz']); 13 | * ``` 14 | * 15 | * **Result** 16 | * 17 | * ``` 18 | * true 19 | * ``` 20 | * 21 | * @param array|\stdClass $collection of key values pairs 22 | * @param array $keys collection of keys to look for 23 | * @param bool $strict to exclusively check 24 | * 25 | * @return bool 26 | */ 27 | function hasKeys($collection = [], array $keys = [], $strict = false) 28 | { 29 | $keyCount = count($keys); 30 | if ($strict && count($collection) !== $keyCount) { 31 | return false; 32 | } 33 | return \__::every( 34 | \__::map($keys, function ($key) use ($collection) { 35 | return \__::has($collection, $key); 36 | }), 37 | function ($v) { 38 | return $v === true; 39 | } 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/__/collections/size.php: -------------------------------------------------------------------------------- 1 | assertEquals('cba', $x); 25 | } 26 | 27 | public function testReduceRightIterable() 28 | { 29 | if (version_compare(PHP_VERSION, '5.5.0', '>=')) { 30 | // Arrange 31 | $a = new ArrayIterator(['a', 'b', 'c']); 32 | $aReducer = function ($word, $char) { 33 | return $word . $char; 34 | }; 35 | 36 | // Act 37 | $x = __::reduceRight($a, $aReducer, ''); 38 | 39 | // Assert 40 | $this->assertEquals('cba', $x); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/__/collections/where.php: -------------------------------------------------------------------------------- 1 | 'fred', 'age' => 32], 13 | * ['name' => 'maciej', 'age' => 16] 14 | * ]; 15 | * 16 | * __::where($a, ['age' => 16]); 17 | * ``` 18 | * 19 | * **Result** 20 | * 21 | * ``` 22 | * [['name' => 'maciej', 'age' => 16]] 23 | * ``` 24 | * 25 | * @todo: implement compatibility with more than 2 dimensional arrays: 26 | * 27 | * @param array|iterable $array array of values 28 | * @param array $cond condition in format of ['KEY'=>'VALUE'] 29 | * 30 | * @see find 31 | * @see findIndex 32 | * @see findLast 33 | * @see findLastIndex 34 | * @see where 35 | * 36 | * @return array 37 | */ 38 | function where($array = [], array $cond = []) 39 | { 40 | $result = []; 41 | foreach ($array as $arrItem) { 42 | foreach ($cond as $condK => $condV) { 43 | if (!isset($arrItem[$condK]) || $arrItem[$condK] !== $condV) { 44 | continue 2; 45 | } 46 | } 47 | $result[] = $arrItem; 48 | } 49 | 50 | return $result; 51 | } 52 | -------------------------------------------------------------------------------- /tests/__/Collections/DoForEachRightTest.php: -------------------------------------------------------------------------------- 1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus']; 25 | 26 | // Act. 27 | $aAppend = []; 28 | $aMapped = []; 29 | $bMapped = []; 30 | __::doForEachRight($a, $makeAppend($aAppend)); 31 | __::doForEachRight($a, $makeMapper($aMapped)); 32 | __::doForEachRight($b, $makeMapper($bMapped)); 33 | 34 | // Assert 35 | $this->assertEquals(array_reverse($a), $aAppend); 36 | $this->assertEquals($a, $aMapped); 37 | $this->assertEquals($b, $bMapped); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/__/collections/map.php: -------------------------------------------------------------------------------- 1 | $item) { 16 | yield $iteratee($item, $key, $collection); 17 | } 18 | } 19 | 20 | /** 21 | * Returns an array of values by mapping each in collection through the iteratee. 22 | * 23 | * **Usage** 24 | * 25 | * ```php 26 | * __::map([1, 2, 3], function($value, $key, $collection) { 27 | * return $value * 3; 28 | * }); 29 | * ``` 30 | * 31 | * **Result** 32 | * 33 | * ``` 34 | * [3, 6, 9] 35 | * ``` 36 | * 37 | * @since 0.2.0 added support for iterables 38 | * 39 | * @param iterable|\stdClass $collection The collection of values to map over. 40 | * @param \Closure $iteratee The function to apply on each value. 41 | * 42 | * @return array|\Generator 43 | */ 44 | function map($collection, \Closure $iteratee) 45 | { 46 | if (is_array($collection) || $collection instanceof \stdClass) { 47 | return iterator_to_array(mapIterable($collection, $iteratee)); 48 | } 49 | 50 | return mapIterable($collection, $iteratee); 51 | } 52 | -------------------------------------------------------------------------------- /src/__/collections/reduceRight.php: -------------------------------------------------------------------------------- 1 | 1, 16 | * 'b' => ['c' => 3, 'd' => 4] 17 | * ], 18 | * ['a', 'b.d'] 19 | * ); 20 | * ``` 21 | * 22 | * **Result** 23 | * 24 | * ``` 25 | * [ 26 | * 'a' => 1, 27 | * 'b' => ['d' => 4] 28 | * ] 29 | * ``` 30 | * 31 | * @since 0.2.0 added support for iterables 32 | * 33 | * @param iterable|\stdClass $collection The collection to iterate over. 34 | * @param array $paths Array paths to pick 35 | * @param mixed $default The default value that will be used if the specified path does not exist. 36 | * 37 | * @return array 38 | */ 39 | function pick($collection = [], array $paths = [], $default = null) 40 | { 41 | return \__::reduce($paths, function ($results, $path) use ($collection, $default) { 42 | return \__::set($results, $path, \__::get($collection, $path, $default)); 43 | }, \__::isObject($collection) && !($collection instanceof \ArrayAccess) ? new \stdClass() : []); 44 | } 45 | -------------------------------------------------------------------------------- /src/__/arrays/patch.php: -------------------------------------------------------------------------------- 1 | [ 14 | * 'country' => 'US', 15 | * 'zip' => 12345 16 | * ] 17 | * ], 18 | * [ 19 | * '/addr/country' => 'CA', 20 | * '/addr/zip' => 54321 21 | * ] 22 | * ); 23 | * ``` 24 | * 25 | * **Result** 26 | * 27 | * ``` 28 | * ['addr' => ['country' => 'CA', 'zip' => 54321]] 29 | * ``` 30 | * 31 | * @param array $array The array to patch 32 | * @param array $patches List of new xpath-value pairs 33 | * @param string $parent 34 | * 35 | * @return array Returns patched array 36 | */ 37 | function patch($array, $patches, $parent = '') 38 | { 39 | $parent .= '/'; 40 | 41 | foreach ($array as $key => &$value) { 42 | $z = $parent . $key; 43 | 44 | if (isset($patches[$z])) { 45 | $value = $patches[$z]; 46 | unset($patches[$z]); 47 | 48 | if (!count($patches)) { 49 | break; 50 | } 51 | } 52 | 53 | if (\is_array($value)) { 54 | $value = patch($array[$key], $patches, $z); 55 | } 56 | } 57 | 58 | return $array; 59 | } 60 | -------------------------------------------------------------------------------- /src/__/collections/mapValues.php: -------------------------------------------------------------------------------- 1 | $value) { 16 | yield $key => call_user_func_array($closure, [$value, $key, $iterable]); 17 | } 18 | } 19 | 20 | /** 21 | * Transforms the values in a collection by running each value through the iterator. 22 | * 23 | * **Usage** 24 | * 25 | * ```php 26 | * __::mapValues(['x' => 1], function($value, $key, $collection) { 27 | * return "{$key}_{$value}"; 28 | * }); 29 | * ``` 30 | * 31 | * **Result** 32 | * 33 | * ``` 34 | * ['x' => 'x_1'] 35 | * ``` 36 | * 37 | * @since 0.2.0 added support for iterables 38 | * 39 | * @param iterable $iterable Array of values 40 | * @param \Closure|null $closure Closure to map the values 41 | * 42 | * @return array|\Generator 43 | */ 44 | function mapValues($iterable, \Closure $closure = null) 45 | { 46 | if (is_null($closure)) { 47 | $closure = '__::identity'; 48 | } 49 | 50 | if (is_array($iterable)) { 51 | return iterator_to_array(mapValuesIterable($iterable, $closure)); 52 | } 53 | 54 | return mapValuesIterable($iterable, $closure); 55 | } 56 | -------------------------------------------------------------------------------- /src/__/collections/merge.php: -------------------------------------------------------------------------------- 1 | ['favorite' => 'red', 'model' => 3, 5], 3], 18 | * [10, 'color' => ['favorite' => 'green', 'blue']] 19 | * ); 20 | * ``` 21 | * 22 | * **Result** 23 | * 24 | * ``` 25 | * ['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10] 26 | * ``` 27 | * 28 | * @param iterable|\stdClass ...$_ Collections to merge. 29 | * 30 | * @return array|object Concatenated collection. 31 | */ 32 | function merge() 33 | { 34 | return \__::reduceRight(func_get_args(), function ($source, $result) { 35 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) { 36 | $value = $sourceValue; 37 | if (\__::isCollection($value)) { 38 | $value = merge(\__::get($result, $key), $sourceValue); 39 | } 40 | $result = \__::set($result, $key, $value); 41 | }); 42 | return $result; 43 | }, []); 44 | } 45 | -------------------------------------------------------------------------------- /src/__/collections/every.php: -------------------------------------------------------------------------------- 1 | [ 20 | * 'favorite' => 'red', 21 | * 5 22 | * ], 23 | * 3 24 | * ], 25 | * [ 26 | * 10, 27 | * 'color' => [ 28 | * 'favorite' => 'green', 29 | * 'blue' 30 | * ] 31 | * ] 32 | * ); 33 | * ``` 34 | * 35 | * **Result** 36 | * 37 | * ``` 38 | * [ 39 | * 'color' => ['favorite' => 'green', 'blue'], 40 | * 10 41 | * ] 42 | * ``` 43 | * 44 | * @param array|object ...$_ Collections to assign. 45 | * 46 | * @return array|object Assigned collection. 47 | */ 48 | function assign($_) 49 | { 50 | return \__::reduceRight(func_get_args(), function ($source, $result) { 51 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) { 52 | $result = \__::set($result, $key, $sourceValue); 53 | }); 54 | return $result; 55 | }, []); 56 | } 57 | -------------------------------------------------------------------------------- /tests/__/Collections/HasTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 15 | $b = (object)['foo' => 'bar']; 16 | $c = ['foo' => ['bar' => 'foie']]; 17 | $d = [5]; 18 | $e = (object)[5]; 19 | 20 | // Act. 21 | $x = __::has($a, 'foo'); 22 | $y = __::has($a, 'foz'); 23 | $z = __::has($b, 'foo'); 24 | $xa = __::has($b, 'foz'); 25 | $xb = __::has($c, 'foo.bar'); 26 | $xc = __::has($d, 0); 27 | $xd = __::has($e, 0); 28 | 29 | // Assert. 30 | $this->assertTrue($x); 31 | $this->assertFalse($y); 32 | $this->assertTrue($z); 33 | $this->assertFalse($xa); 34 | $this->assertTrue($xb); 35 | $this->assertTrue($xc); 36 | $this->assertTrue($xd); 37 | } 38 | 39 | public function testHasArrayAccess() 40 | { 41 | $aa = new ArrayAccessible(); 42 | $aa['qux'] = true; 43 | $aa['field'] = null; 44 | 45 | $this->assertTrue(__::has($aa, 'qux')); 46 | $this->assertTrue(__::has($aa, 'field')); 47 | $this->assertFalse(__::has($aa, 'non-existent')); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/__/collections/ease.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (is_array($value)) { 19 | _ease($map, $value, $glue, $prefix . $index . $glue); 20 | } else { 21 | $map[$prefix . $index] = $value; 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * Flattens a complex collection by mapping each ending leafs value to a key 28 | * consisting of all previous indexes. 29 | * 30 | * **Usage** 31 | * 32 | * ```php 33 | * __::ease([ 34 | * 'foo' => ['bar' => 'ter'], 35 | * 'baz' => ['b', 'z'] 36 | * ]); 37 | * ``` 38 | * 39 | * **Result** 40 | * 41 | * ``` 42 | * [ 43 | * 'foo.bar' => 'ter', 44 | * 'baz.0' => 'b', 45 | * 'baz.1' => 'z' 46 | * ] 47 | * ``` 48 | * 49 | * @since 0.2.0 added support for iterables 50 | * 51 | * @param iterable $collection array of values 52 | * @param string $glue glue between key path 53 | * 54 | * @return array flatten collection 55 | */ 56 | function ease($collection, $glue = '.') 57 | { 58 | $map = []; 59 | _ease($map, $collection, $glue); 60 | 61 | return $map; 62 | } 63 | -------------------------------------------------------------------------------- /src/__/collections/some.php: -------------------------------------------------------------------------------- 1 | 1, 'rungis' => [2, 3]], ['honfleur' => 1, 'rungis' => [1, 2]]); 17 | * ``` 18 | * 19 | * **Result** 20 | * 21 | * ``` 22 | * false 23 | * ``` 24 | * 25 | * @param mixed $object1 26 | * @param mixed $object2 27 | * 28 | * @return bool 29 | */ 30 | function isEqual($object1, $object2) 31 | { 32 | if (\__::isCollection($object1)) { 33 | // Is not equal if number of keys differ. 34 | $object1Keys = \__::isObject($object1) ? array_keys(get_object_vars($object1)) : array_keys($object1); 35 | $object2Keys = \__::isObject($object2) ? array_keys(get_object_vars($object2)) : array_keys($object2); 36 | if (count($object1Keys) !== count($object2Keys)) { 37 | return false; 38 | } 39 | foreach ($object1 as $key1 => $value1) { 40 | if (!\__::has($object2, $key1) || !\__::isEqual($value1, \__::get($object2, $key1))) { 41 | return false; 42 | } 43 | } 44 | return true; 45 | } 46 | return $object1 === $object2; 47 | } 48 | -------------------------------------------------------------------------------- /src/__/functions/urlify.php: -------------------------------------------------------------------------------- 1 | google.com' 18 | * ``` 19 | * 20 | * @param string $string 21 | * 22 | * @return string 23 | */ 24 | function urlify($string) 25 | { 26 | /* 27 | * Proposed by: 28 | * Søren Løvborg 29 | * http://stackoverflow.com/users/136796/soren-lovborg 30 | * http://stackoverflow.com/questions/17900004/turn-plain-text-urls-into-active-links-using-php/17900021#17900021 31 | */ 32 | 33 | $rexProtocol = '(https?://)?'; 34 | $rexDomain = '((?:[-a-zA-Z0-9]{1,63}\.)+[-a-zA-Z0-9]{2,63}|(?:[0-9]{1,3}\.){3}[0-9]{1,3})'; 35 | $rexPort = '(:[0-9]{1,5})?'; 36 | $rexPath = '(/[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]*?)?'; 37 | $rexQuery = '(\?[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?'; 38 | $rexFragment = '(#[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?'; 39 | 40 | return preg_replace_callback( 41 | "&\\b$rexProtocol$rexDomain$rexPort$rexPath$rexQuery$rexFragment(?=[?.!,;:\"]?(\s|$))&", 42 | function ($match) { 43 | $completeUrl = $match[1] ? $match[0] : "http://{$match[0]}"; 44 | 45 | return '' . $match[2] . $match[3] . $match[4] . ''; 46 | }, 47 | htmlspecialchars($string) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /tests/__/Collections/AssignTest.php: -------------------------------------------------------------------------------- 1 | ['favorite' => 'red', 'model' => 3, 5], 3]; 14 | $a2 = [10, 'color' => ['favorite' => 'green', 'blue']]; 15 | $b1 = ['a' => 0]; 16 | $b2 = ['a' => 1, 'b' => 2]; 17 | $b3 = ['c' => 3, 'd' => 4]; 18 | 19 | // Act 20 | $x = __::assign($a1, $a2); 21 | $y = __::assign($b1, $b2, $b3); 22 | 23 | // Assert 24 | $this->assertEquals(['color' => ['favorite' => 'green', 'blue'], 10], $x); 25 | $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $y); 26 | } 27 | 28 | public function testAssignObject() 29 | { 30 | // Arrange 31 | $a1 = (object)['color' => (object)['favorite' => 'red', 5]]; 32 | $a2 = (object)[10, 'color' => (object)['favorite' => 'green', 'blue']]; 33 | $b1 = (object)['a' => 0]; 34 | $b2 = (object)['a' => 1, 'b' => 2, 5]; 35 | $b3 = (object)['c' => 3, 'd' => 4, 6]; 36 | 37 | // Act 38 | $x = __::assign($a1, $a2); 39 | $y = __::assign($b1, $b2, $b3); 40 | 41 | // Assert 42 | $this->assertEquals((object)['color' => (object)['favorite' => 'green', 'blue'], 10], $x); 43 | $this->assertEquals((object)['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 6], $y); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/__/Collections/ReverseIterableTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($x instanceof \Generator); 22 | $xValues = []; 23 | foreach ($x as $value) { 24 | $xValues[] = $value; 25 | } 26 | $this->assertEquals([3, 2, 1], $xValues); 27 | 28 | // Note how the keys have been preserved and inverted. 29 | // TODO Add an option preserve_keys as array_reverse or iterator_to_array? 30 | $this->assertEquals([2 => 3, 1 => 2, 0 => 1], iterator_to_array(__::reverseIterable($a))); 31 | $this->assertEquals([3, 2, 1], iterator_to_array(__::reverseIterable($a), false)); 32 | } 33 | 34 | public function testReverseIterableArrayIterable() 35 | { 36 | // Arrange 37 | $a = new ArrayIterator([1, 2, 3]); 38 | 39 | // Act 40 | $x = __::reverseIterable($a); 41 | 42 | // Assert 43 | // Check we got back a Generator. 44 | $this->assertTrue($x instanceof \Generator); 45 | $xValues = []; 46 | foreach ($x as $value) { 47 | $xValues[] = $value; 48 | } 49 | $this->assertEquals([3, 2, 1], $xValues); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/__/Arrays/CompactTest.php: -------------------------------------------------------------------------------- 1 | [0, 1, false, 2, '', 3], 17 | 'expected' => [1, 2, 3], 18 | ], 19 | [ 20 | 'sourceArray' => new ArrayIterator([0, 1, false, 2, '', 3]), 21 | 'expected' => [1, 2, 3], 22 | ], 23 | [ 24 | 'sourceArray' => new MockIteratorAggregate([0, 1, false, 2, '', 3]), 25 | 'expected' => [1, 2, 3], 26 | ], 27 | [ 28 | 'sourceArray' => call_user_func(function () { 29 | yield 0; 30 | yield 1; 31 | yield false; 32 | yield 2; 33 | yield ''; 34 | yield 3; 35 | }), 36 | 'expected' => [1, 2, 3], 37 | ], 38 | ]; 39 | } 40 | 41 | /** 42 | * @dataProvider provideCompactCases 43 | * 44 | * @param array|iterable $sourceArray 45 | * @param array $expected 46 | */ 47 | public function testCompact($sourceArray, $expected) 48 | { 49 | $actual = __::compact($sourceArray); 50 | 51 | foreach ($actual as $i => $item) { 52 | $this->assertEquals($expected[$i], $item); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/__/Sequences/ChainTest.php: -------------------------------------------------------------------------------- 1 | compact() 20 | ->prepend(4) 21 | ->value(); 22 | 23 | // Assert 24 | $this->assertEquals([4, 1, 2, 3], $result); 25 | } 26 | 27 | public function testChainReturnsNumber() 28 | { 29 | // Arrange 30 | $collection = [0, 1, 2, 3, null]; 31 | 32 | // Act 33 | $result = __::chain($collection) 34 | ->compact() 35 | ->prepend(4) 36 | ->reduce(function ($sum, $number) { 37 | return $sum + $number; 38 | }, 0) 39 | ->value(); 40 | 41 | // Assert 42 | $this->assertEquals(10, $result); 43 | } 44 | 45 | public function testChainWithError() 46 | { 47 | if (method_exists($this, 'expectException')) { 48 | // new phpunit 49 | $this->expectException('\Exception'); 50 | } else { 51 | // old phpunit 52 | $this->setExpectedException('\Exception'); 53 | } 54 | 55 | // Arrange 56 | $collection = [0, 1, 2, 3, null]; 57 | 58 | // Act 59 | __::chain($collection) 60 | ->randomFunc() 61 | ->value(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/__/Collections/HasKeysTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 14 | $b = ['foo' => ['bar' => 'foie'], 'estomac' => true]; 15 | 16 | // Act 17 | $x = __::hasKeys($a, ['foo', 'foz'], false); 18 | $y = __::hasKeys($a, ['foo', 'foz'], true); 19 | $z = __::hasKeys($b, ['foo.bar', 'estomac']); 20 | 21 | // Assert 22 | $this->assertFalse($x); 23 | $this->assertFalse($y); 24 | $this->assertTrue($z); 25 | 26 | // Rearrange 27 | $a['foz'] = 'baz'; 28 | 29 | // React 30 | $x = __::hasKeys($a, ['foo', 'foz'], false); 31 | $y = __::hasKeys($a, ['foo', 'foz'], true); 32 | 33 | // Assert 34 | $this->assertTrue($x); 35 | $this->assertTrue($y); 36 | 37 | // Rearrange 38 | $a['xxx'] = 'bay'; 39 | 40 | // React 41 | $x = __::hasKeys($a, ['foo', 'foz'], false); 42 | $y = __::hasKeys($a, ['foo', 'foz'], true); 43 | 44 | // Assert 45 | $this->assertTrue($x); 46 | $this->assertFalse($y); 47 | } 48 | 49 | public function testHasKeysObject() 50 | { 51 | // Arrange. 52 | $a = (object)['foo' => 'bar']; 53 | 54 | // Act 55 | $x = __::hasKeys($a, ['foo']); 56 | $y = __::hasKeys($a, ['foo', 'foz']); 57 | 58 | // Assert 59 | $this->assertTrue($x); 60 | $this->assertFalse($y); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/__/collections/has.php: -------------------------------------------------------------------------------- 1 | ['bar' => 'num'], 'foz' => 'baz'], 'foo.bar'); 14 | * ``` 15 | * 16 | * **Result** 17 | * 18 | * ``` 19 | * true 20 | * ``` 21 | * 22 | * **Object Usage** 23 | * 24 | * ```php 25 | * __::hasKeys((object) ['foo' => 'bar', 'foz' => 'baz'], 'bar'); 26 | * ``` 27 | * 28 | * **Result** 29 | * 30 | * ``` 31 | * false 32 | * ``` 33 | * 34 | * @param array|object $collection Array or object to search a key for 35 | * @param string|int $path Path to look for. Supports dot notation for traversing multiple levels. 36 | * 37 | * @return bool 38 | */ 39 | function has($collection, $path) 40 | { 41 | $portions = \__::split($path, \__::DOT_NOTATION_DELIMITER, 2); 42 | $key = $portions[0]; 43 | 44 | if (\count($portions) === 1) { 45 | // Calling array_key_exists on an ArrayAccess object will not call `offsetExists()` 46 | // See: http://php.net/manual/en/class.arrayaccess.php#104061 47 | if ($collection instanceof \ArrayAccess) { 48 | return $collection->offsetExists($key); 49 | } 50 | 51 | // We use a cast to array to handle the numeric keys for objects (workaround). 52 | // See: https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts 53 | return array_key_exists($key, (array)$collection); 54 | } 55 | 56 | return has(\__::get($collection, $key), $portions[1]); 57 | } 58 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('./vendor/') 5 | ->in(__DIR__) 6 | ; 7 | 8 | $config = new PhpCsFixer\Config(); 9 | $config 10 | ->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers()) 11 | ->setRiskyAllowed(true) 12 | ->setRules([ 13 | '@PSR1' => true, 14 | '@PSR2' => true, 15 | PhpCsFixerCustomFixers\Fixer\CommentSurroundedBySpacesFixer::name() => true, 16 | PhpCsFixerCustomFixers\Fixer\DataProviderNameFixer::name() => true, 17 | PhpCsFixerCustomFixers\Fixer\DataProviderStaticFixer::name() => true, 18 | PhpCsFixerCustomFixers\Fixer\NoDuplicatedImportsFixer::name() => true, 19 | PhpCsFixerCustomFixers\Fixer\NoPhpStormGeneratedCommentFixer::name() => true, 20 | PhpCsFixerCustomFixers\Fixer\NoUselessParenthesisFixer::name() => true, 21 | PhpCsFixerCustomFixers\Fixer\PhpUnitAssertArgumentsOrderFixer::name() => true, 22 | PhpCsFixerCustomFixers\Fixer\PhpUnitDedicatedAssertFixer::name() => true, 23 | PhpCsFixerCustomFixers\Fixer\PhpUnitNoUselessReturnFixer::name() => true, 24 | PhpCsFixerCustomFixers\Fixer\PhpdocNoIncorrectVarAnnotationFixer::name() => true, 25 | PhpCsFixerCustomFixers\Fixer\PhpdocParamTypeFixer::name() => true, 26 | PhpCsFixerCustomFixers\Fixer\PhpdocSelfAccessorFixer::name() => true, 27 | PhpCsFixerCustomFixers\Fixer\PhpdocSingleLineVarFixer::name() => true, 28 | PhpCsFixerCustomFixers\Fixer\PhpdocTypesTrimFixer::name() => true, 29 | PhpCsFixerCustomFixers\Fixer\StringableInterfaceFixer::name() => true, 30 | ]) 31 | ->setFinder($finder) 32 | ; 33 | 34 | return $config; 35 | -------------------------------------------------------------------------------- /tests/__/Collections/FindTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("explain", __::find($data, "explain")); 14 | $this->assertNull(__::find($data, "nonexistent")); 15 | } 16 | 17 | public function testWithAssociativeArray() 18 | { 19 | $data = [ 20 | "table" => "trick", 21 | "pen" => "defend", 22 | "motherly" => "wide", 23 | "may" => "needle", 24 | "sweat" => "cake", 25 | "sword" => "defend", 26 | ]; 27 | 28 | $this->assertEquals("defend", __::find($data, "defend")); 29 | $this->assertNull(__::find($data, "nonexistent")); 30 | } 31 | 32 | public function testWithCallback() 33 | { 34 | $data = [ 35 | "table" => (object)["name" => "trick"], 36 | "pen" => (object)["name" => "defend"], 37 | "motherly" => (object)["name" => "wide"], 38 | "may" => (object)["name" => "needle"], 39 | "sweat" => (object)["name" => "cake"], 40 | "sword" => (object)["name" => "defend"], 41 | ]; 42 | 43 | $this->assertEquals($data["pen"], __::find($data, static function ($object) { 44 | return $object->name === "defend"; 45 | })); 46 | $this->assertNull(__::find($data, static function ($value) { 47 | return $value === "potato"; 48 | })); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/__/Collections/FindLastTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("explain", __::findLast($data, "explain")); 14 | $this->assertNull(__::findLast($data, "nonexistent")); 15 | } 16 | 17 | public function testWithAssociativeArray() 18 | { 19 | $data = [ 20 | "table" => "trick", 21 | "pen" => "defend", 22 | "motherly" => "wide", 23 | "may" => "needle", 24 | "sweat" => "cake", 25 | "sword" => "defend", 26 | ]; 27 | 28 | $this->assertEquals("defend", __::findLast($data, "defend")); 29 | $this->assertNull(__::findLast($data, "nonexistent")); 30 | } 31 | 32 | public function testWithCallback() 33 | { 34 | $data = [ 35 | "table" => (object)["name" => "trick"], 36 | "pen" => (object)["name" => "defend"], 37 | "motherly" => (object)["name" => "wide"], 38 | "may" => (object)["name" => "needle"], 39 | "sweat" => (object)["name" => "cake"], 40 | "sword" => (object)["name" => "defend"], 41 | ]; 42 | 43 | $this->assertEquals($data["sword"], __::findLast($data, static function ($object) { 44 | return $object->name === "defend"; 45 | })); 46 | $this->assertNull(__::findLast($data, static function ($value) { 47 | return $value === "potato"; 48 | })); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/__/Collections/WhereTest.php: -------------------------------------------------------------------------------- 1 | 'fred', 'age' => 32], 17 | ['name' => 'maciej', 'age' => 16], 18 | ['a' => 'b', 'c' => 'd'] 19 | ]; 20 | 21 | // Act 22 | $x = __::where($a, ['age' => 16]); 23 | $x2 = __::where($a, ['age' => 16, 'name' => 'fred']); 24 | $x3 = __::where($a, ['name' => 'maciej', 'age' => 16]); 25 | $x4 = __::where($a, ['name' => 'unknown']); 26 | 27 | // Assert 28 | $this->assertEquals([$a[1]], $x); 29 | $this->assertEquals([], $x2); 30 | $this->assertEquals([$a[1]], $x3); 31 | $this->assertEquals([], $x4); 32 | } 33 | 34 | public function testWhereIterable() 35 | { 36 | // Arrange 37 | $a = new ArrayIterator([ 38 | ['name' => 'fred', 'age' => 32], 39 | ['name' => 'maciej', 'age' => 16], 40 | ['a' => 'b', 'c' => 'd'] 41 | ]); 42 | 43 | // Act 44 | $x = __::where($a, ['age' => 16]); 45 | $x2 = __::where($a, ['age' => 16, 'name' => 'fred']); 46 | $x3 = __::where($a, ['name' => 'maciej', 'age' => 16]); 47 | $x4 = __::where($a, ['name' => 'unknown']); 48 | 49 | // Assert 50 | $this->assertEquals([$a[1]], $x); 51 | $this->assertEquals([], $x2); 52 | $this->assertEquals([$a[1]], $x3); 53 | $this->assertEquals([], $x4); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/__/collections/pluck.php: -------------------------------------------------------------------------------- 1 | 'bar', 'bis' => 'ter' ], 13 | * ['foo' => 'bar2', 'bis' => 'ter2'], 14 | * ]; 15 | * 16 | * __::pluck($a, 'foo'); 17 | * ``` 18 | * 19 | * **Result** 20 | * 21 | * ``` 22 | * ['bar', 'bar2'] 23 | * ``` 24 | * 25 | * @since 0.2.0 added support for iterables 26 | * 27 | * @param iterable|\stdClass $collection Array or object that can be converted to array 28 | * @param string $property property name 29 | * 30 | * @return array 31 | */ 32 | function pluck($collection, $property) 33 | { 34 | $result = array_map(function ($value) use ($property) { 35 | if (is_array($value) && isset($value[$property])) { 36 | return $value[$property]; 37 | } elseif (is_object($value) && isset($value->{$property})) { 38 | return $value->{$property}; 39 | } 40 | 41 | foreach (\__::split($property, \__::DOT_NOTATION_DELIMITER) as $segment) { 42 | if (is_object($value)) { 43 | if (isset($value->{$segment})) { 44 | $value = $value->{$segment}; 45 | } else { 46 | return null; 47 | } 48 | } else { 49 | if (isset($value[$segment])) { 50 | $value = $value[$segment]; 51 | } else { 52 | return null; 53 | } 54 | } 55 | } 56 | 57 | return $value; 58 | }, (array)$collection); 59 | 60 | return array_values($result); 61 | } 62 | -------------------------------------------------------------------------------- /src/__/arrays/drop.php: -------------------------------------------------------------------------------- 1 | "Having at least one even number should return true", 15 | 'actual' => [1, 3, 5, 10, 7, 9], 16 | 'callback' => static function ($item) { 17 | return $item % 2 === 0; 18 | }, 19 | 'expected' => true 20 | ], 21 | [ 22 | 'description' => "Having no even numbers should return false", 23 | 'actual' => [1, 3, 5, 7, 9], 24 | 'callback' => static function ($item) { 25 | return $item % 2 === 0; 26 | }, 27 | 'expected' => false 28 | ], 29 | [ 30 | 'description' => "Having at least one truthy value should return true", 31 | 'actual' => [false, [], 1], 32 | 'callback' => null, 33 | 'expected' => true 34 | ], 35 | [ 36 | 'description' => "Having all falsey values should return false", 37 | 'actual' => [false, [], 0], 38 | 'callback' => null, 39 | 'expected' => false 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * @dataProvider provideSameCases 46 | */ 47 | public function testSame($message, $actual, $callback, $expected) 48 | { 49 | $actual = __::some($actual, $callback); 50 | 51 | $this->assertSame($expected, $actual, $message); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Bottomline logo 3 | Bottomline logo 4 |
5 | 6 | # bottomline 7 | 8 | [![Tests](https://github.com/maciejczyzewski/bottomline/actions/workflows/ci.yml/badge.svg)](https://github.com/maciejczyzewski/bottomline/actions/workflows/ci.yml) 9 | ![GitHub](https://img.shields.io/github/license/maciejczyzewski/bottomline) 10 | [![Packagist Version](https://img.shields.io/packagist/v/maciejczyzewski/bottomline)](https://packagist.org/packages/maciejczyzewski/bottomline) 11 | [![Packagist Version](https://img.shields.io/packagist/v/maciejczyzewski/bottomline?include_prereleases)](https://packagist.org/packages/maciejczyzewski/bottomline) 12 | 13 | bottomline is a PHP utility library, similar to Underscore/Lodash, that utilizes namespaces and dynamic autoloading to improve performance. 14 | 15 | **NOTE:** bottomline is not currently in feature parity with Underscore/Lodash. Review the [contributing](./CONTRIBUTING.md) section for more information. 16 | 17 | [View the documentation »](https://maciejczyzewski.github.io/bottomline/) 18 | 19 | ## Requirements 20 | 21 | - PHP 5.5+ 22 | - mbstring 23 | 24 | ## Benchmarks 25 | 26 |
27 | 28 |
29 | 30 | ## Thanks 31 | 32 | * Yoan Tournade ([@MonsieurV](https://github.com/MonsieurV)) 33 | * Vladimir Jimenez ([@allejo](https://github.com/allejo)) 34 | * Brandtley McMinn ([@bmcminn](https://github.com/bmcminn)) 35 | * Ivan Ternovtsiy ([@diaborn19](https://github.com/diaborn19)) 36 | * Tobias Seipke ([@nullpunkt](https://github.com/nullpunkt)) 37 | 38 | ## License 39 | 40 | [MIT](./LICENSE) 41 | -------------------------------------------------------------------------------- /tests/__/Collections/FindLastIndexTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(5, __::findLastIndex($data, "explain")); 14 | $this->assertEquals(-1, __::findLastIndex($data, "nonexistent")); 15 | } 16 | 17 | public function testWithAssociativeArray() 18 | { 19 | $data = [ 20 | "table" => "trick", 21 | "pen" => "defend", 22 | "motherly" => "wide", 23 | "may" => "needle", 24 | "sweat" => "cake", 25 | "sword" => "defend", 26 | ]; 27 | 28 | $this->assertEquals("sword", __::findLastIndex($data, "defend")); 29 | $this->assertEquals(-1, __::findLastIndex($data, "nonexistent")); 30 | } 31 | 32 | public function testWithCallback() 33 | { 34 | $data = [ 35 | "table" => (object)["name" => "trick"], 36 | "pen" => (object)["name" => "defend"], 37 | "motherly" => (object)["name" => "wide"], 38 | "may" => (object)["name" => "needle"], 39 | "sweat" => (object)["name" => "cake"], 40 | "sword" => (object)["name" => "defend"], 41 | ]; 42 | 43 | $this->assertEquals("sword", __::findLastIndex($data, static function ($object) { 44 | return $object->name === "defend"; 45 | })); 46 | $this->assertEquals(-1, __::findLastIndex($data, static function ($value) { 47 | return $value === "potato"; 48 | })); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/__/Collections/LastTest.php: -------------------------------------------------------------------------------- 1 | [1, 2, 3, 4, 5], 17 | 'take' => 2, 18 | 'expected' => [4, 5], 19 | ], 20 | [ 21 | 'source' => [1, 2, 3, 4, 5], 22 | 'take' => null, 23 | 'expected' => 5, 24 | ], 25 | [ 26 | 'source' => new MockIteratorAggregate([1, 2, 3, 4, 5]), 27 | 'take' => 3, 28 | 'expected' => [3, 4, 5], 29 | ], 30 | [ 31 | 'source' => new ArrayIterator([1, 2, 3, 4, 5]), 32 | 'take' => 4, 33 | 'expected' => [2, 3, 4, 5], 34 | ], 35 | [ 36 | 'source' => call_user_func(function () { 37 | yield 1; 38 | yield 2; 39 | yield 3; 40 | yield 4; 41 | yield 5; 42 | }), 43 | 'take' => 2, 44 | 'expected' => [4, 5], 45 | ], 46 | ]; 47 | } 48 | 49 | /** 50 | * @dataProvider provideLastCases 51 | * 52 | * @param iterable $source 53 | * @param int|null $take 54 | * @param array $expected 55 | */ 56 | public function testLast($source, $take, $expected) 57 | { 58 | $actual = __::last($source, $take); 59 | 60 | $this->assertEquals($expected, $actual); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/__/collections/mapKeys.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | // For PHP 5.5 support, this can't be replaced with `$closure($key, ...)` 19 | $newKey = call_user_func_array($closure, array($key, $value, $iterable)); 20 | 21 | // key must be a number or string 22 | if (!is_numeric($newKey) && !is_string($newKey)) { 23 | throw new \InvalidArgumentException('closure must returns a number or string'); 24 | } 25 | 26 | yield $newKey => $value; 27 | } 28 | } 29 | 30 | /** 31 | * Transforms the keys in a collection by running each key through the iterator. 32 | * 33 | * This function throws an `\Exception` when the closure doesn't return a valid 34 | * key that can be used in a PHP array. 35 | * 36 | * **Usage** 37 | * 38 | * ```php 39 | * __::mapKeys(['x' => 1], function($key, $value, $collection) { 40 | * return "{$key}_{$value}"; 41 | * }); 42 | * ``` 43 | * 44 | * **Result** 45 | * 46 | * ``` 47 | * ['x_1' => 1] 48 | * ``` 49 | * 50 | * @since 0.2.0 added support for iterables 51 | * 52 | * @param iterable $iterable Array/iterable of values 53 | * @param \Closure|null $closure Closure to map the keys 54 | * 55 | * @throws \InvalidArgumentException when closure doesn't return a valid key that can be used in PHP array 56 | * 57 | * @return array|\Generator 58 | */ 59 | function mapKeys($iterable, \Closure $closure = null) 60 | { 61 | if (is_null($closure)) { 62 | $closure = '__::identity'; 63 | } 64 | 65 | if (is_array($iterable)) { 66 | return iterator_to_array(mapKeysIterable($iterable, $closure)); 67 | } 68 | 69 | return mapKeysIterable($iterable, $closure); 70 | } 71 | -------------------------------------------------------------------------------- /tests/__/Collections/EveryTest.php: -------------------------------------------------------------------------------- 1 | "Passing an array with one or more non-bool values should return false when given a callback to check for booleans", 15 | 'actual' => [true, 1, null, 'yes'], 16 | 'callback' => static function ($v) { 17 | return is_bool($v); 18 | }, 19 | 'expected' => false, 20 | ], 21 | [ 22 | 'description' => "Passing an array with only booleans should return true when given a callback to check for booleans", 23 | 'actual' => [true, false], 24 | 'callback' => static function ($v) { 25 | return is_bool($v); 26 | }, 27 | 'expected' => true, 28 | ], 29 | [ 30 | 'description' => "Passing an array with only integers should return true when given a callback to check for integers", 31 | 'actual' => [1, 3, 4], 32 | 'callback' => static function ($v) { 33 | return is_int($v); 34 | }, 35 | 'expected' => true, 36 | ], 37 | [ 38 | 'description' => "Passing an array with truthy values should return true when given null as the callback", 39 | 'actual' => [1, "hello", true], 40 | 'callback' => null, 41 | 'expected' => true, 42 | ], 43 | ]; 44 | } 45 | 46 | /** 47 | * @dataProvider provideEveryCases 48 | */ 49 | public function testEvery($message, $actual, $callback, $expected) 50 | { 51 | $actual = __::every($actual, $callback); 52 | 53 | $this->assertSame($expected, $actual, $message); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/__/collections/filter.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if ($closure) { 19 | if ($closure($value)) { 20 | yield $value; 21 | } 22 | } elseif ($value) { 23 | yield $value; 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Returns the values in the collection that pass the truth test. 30 | * 31 | * When `$closure` is set to null, this function will automatically remove falsey 32 | * values. When `$closure` is given, then values where the closure returns false 33 | * will be removed. 34 | * 35 | * **Usage** 36 | * 37 | * ```php 38 | * $a = [ 39 | * ['name' => 'fred', 'age' => 32], 40 | * ['name' => 'maciej', 'age' => 16] 41 | * ]; 42 | * 43 | * __::filter($a, function($n) { 44 | * return $n['age'] > 24; 45 | * }); 46 | * ``` 47 | * 48 | * **Result** 49 | * 50 | * ``` 51 | * [['name' => 'fred', 'age' => 32]] 52 | * ``` 53 | * 54 | * @since 0.2.0 iterable objects are now supported 55 | * 56 | * @param iterable $iterable Array to filter 57 | * @param \Closure|null $closure Closure to filter the array 58 | * 59 | * @throws \InvalidArgumentException when an non-array or non-traversable object is given for $iterable. 60 | * 61 | * @return array|\Generator When given a `\Traversable` object for `$iterable`, a generator will be returned. 62 | * Otherwise, an array will be returned. 63 | */ 64 | function filter($iterable, \Closure $closure = null) 65 | { 66 | if (is_array($iterable)) { 67 | return iterator_to_array(filterIterable($iterable, $closure)); 68 | } 69 | 70 | return filterIterable($iterable, $closure); 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | 7 | jobs: 8 | docs: 9 | name: Documentation Kept Updated 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up PHP 8.2 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '8.2' 19 | extensions: mbstring 20 | 21 | - name: Get Composer Cache Directory 22 | id: composer-cache 23 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@v3 27 | with: 28 | path: ${{ steps.composer-cache.outputs.dir }} 29 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 30 | restore-keys: ${{ runner.os }}-composer- 31 | 32 | - name: Install dependencies 33 | run: composer install 34 | 35 | - name: Generate the documentation 36 | run: composer doc 37 | 38 | - name: Check Generated Docs 39 | run: git diff --exit-code 40 | 41 | formatting: 42 | name: Code Formatting 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v3 47 | 48 | - name: Set up PHP 8.2 49 | uses: shivammathur/setup-php@v2 50 | with: 51 | php-version: '8.2' 52 | extensions: mbstring 53 | 54 | - name: Get Composer Cache Directory 55 | id: composer-cache 56 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 57 | 58 | - name: Cache dependencies 59 | uses: actions/cache@v3 60 | with: 61 | path: ${{ steps.composer-cache.outputs.dir }} 62 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 63 | restore-keys: ${{ runner.os }}-composer- 64 | 65 | - name: Install dependencies 66 | run: composer install 67 | 68 | - name: Check formatting 69 | run: composer cs-check 70 | -------------------------------------------------------------------------------- /src/__/collections/findLastEntry.php: -------------------------------------------------------------------------------- 1 | "trick", 13 | * "pen" => "defend", 14 | * "motherly" => "wide", 15 | * "may" => "needle", 16 | * "sweat" => "cake", 17 | * "sword" => "defend", 18 | * ]; 19 | * 20 | * __::findLastEntry($data, "defend"); 21 | * ``` 22 | * 23 | * **Result** 24 | * 25 | * ``` 26 | * ["sword", "defend"] 27 | * ``` 28 | * 29 | * **Find by Callback in an Array** 30 | * 31 | * ```php 32 | * $data = [ 33 | * "table" => (object)["name" => "trick"], 34 | * "pen" => (object)["name" => "defend"], 35 | * "motherly" => (object)["name" => "wide"], 36 | * "may" => (object)["name" => "needle"], 37 | * "sweat" => (object)["name" => "cake"], 38 | * "sword" => (object)["name" => "defend"], 39 | * ]; 40 | * 41 | * __::findLastEntry($data, static function ($object, $key, $collection) { 42 | * return $object->name === "defend"; 43 | * }); 44 | * ``` 45 | * 46 | * **Result** 47 | * 48 | * ``` 49 | * ["sword", $data["sword"]] 50 | * ``` 51 | * 52 | * @since 0.2.1 added to bottomline 53 | * 54 | * @param iterable $collection an array or iterable of values to look through 55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 56 | * 57 | * @see find 58 | * @see findEntry 59 | * @see findLastIndex 60 | * @see findIndex 61 | * @see findLast 62 | * @see where 63 | * 64 | * @return array|null An array with two values, the 0th index is the key and the 1st index is the value. Null is 65 | * returned if no entries can be found in the given collection. 66 | */ 67 | function findLastEntry($collection, $condition) 68 | { 69 | return \__::findEntry(\__::reverseIterable($collection), $condition); 70 | } 71 | -------------------------------------------------------------------------------- /tests/__/Collections/DoForEachTest.php: -------------------------------------------------------------------------------- 1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus']; 20 | $c = (object)['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus']; 21 | 22 | // Act. 23 | $aMapped = []; 24 | $bMapped = []; 25 | $cMapped = []; 26 | __::doForEach($a, $makeMapper($aMapped)); 27 | __::doForEach($b, $makeMapper($bMapped)); 28 | __::doForEach($c, $makeMapper($cMapped)); 29 | 30 | // Assert 31 | $this->assertEquals($a, $aMapped); 32 | $this->assertEquals($b, $bMapped); 33 | $this->assertEquals($c, (object)$cMapped); 34 | $this->assertEquals((array)$c, $cMapped); 35 | } 36 | 37 | public function testDoForEachPrematureReturn() 38 | { 39 | // Arrange 40 | $makeMapper = function (&$array, $returnAtKey) { 41 | return function ($value, $key) use (&$array, $returnAtKey) { 42 | $array[$key] = $value; 43 | if ($returnAtKey === $key) { 44 | return false; 45 | } 46 | }; 47 | }; 48 | $a = [1, 2, 3, 4]; 49 | $b = ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus']; 50 | 51 | // Act. 52 | $aMapped = []; 53 | $bMapped = []; 54 | __::doForEach($a, $makeMapper($aMapped, 1)); 55 | __::doForEach($b, $makeMapper($bMapped, 'city')); 56 | 57 | // Assert 58 | $this->assertEquals([1, 2], $aMapped); 59 | $this->assertEquals(['state' => 'IN', 'city' => 'Indianapolis'], $bMapped); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/__/Collections/EaseTest.php: -------------------------------------------------------------------------------- 1 | ['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']], 18 | 'glue' => '.', 19 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'], 20 | ], 21 | [ 22 | 'source' => ['foo' => ['bar' => $object], 'baz' => ['b', 'z']], 23 | 'glue' => '_', 24 | 'expected' => ['foo_bar' => $object, 'baz_0' => 'b', 'baz_1' => 'z'], 25 | ], 26 | [ 27 | 'source' => new ArrayIterator(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]), 28 | 'glue' => '.', 29 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'], 30 | ], 31 | [ 32 | 'source' => new MockIteratorAggregate(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]), 33 | 'glue' => '.', 34 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'], 35 | ], 36 | [ 37 | 'source' => call_user_func(function () { 38 | yield 'foo' => ['bar' => 'ter']; 39 | yield 'baz' => ['b', 'z']; 40 | }), 41 | 'glue' => '.', 42 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'], 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * @dataProvider provideEaseCases 49 | * 50 | * @param mixed $source 51 | * @param string $glue 52 | * @param array $expected 53 | */ 54 | public function testEase($source, $glue, $expected) 55 | { 56 | $this->assertEquals($expected, __::ease($source, $glue)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/__/arrays/dropRight.php: -------------------------------------------------------------------------------- 1 | "trick", 13 | * "pen" => "defend", 14 | * "motherly" => "wide", 15 | * "may" => "needle", 16 | * "sweat" => "cake", 17 | * "sword" => "defend", 18 | * ]; 19 | * 20 | * __::find($data, "defend"); 21 | * ``` 22 | * 23 | * **Result** 24 | * 25 | * ``` 26 | * "defend" 27 | * ``` 28 | * 29 | * **Find by Callback in an Array** 30 | * 31 | * ```php 32 | * $data = [ 33 | * "table" => (object)["name" => "trick"], 34 | * "pen" => (object)["name" => "defend"], 35 | * "motherly" => (object)["name" => "wide"], 36 | * "may" => (object)["name" => "needle"], 37 | * "sweat" => (object)["name" => "cake"], 38 | * "sword" => (object)["name" => "defend"], 39 | * ]; 40 | * 41 | * __::find($data, static function ($object, $key, $collection) { 42 | * return $object->name === "defend"; 43 | * }); 44 | * ``` 45 | * 46 | * **Result** 47 | * 48 | * ``` 49 | * $data["pen"] 50 | * ``` 51 | * 52 | * @since 0.2.1 added to bottomline 53 | * 54 | * @param array|iterable $collection an array or iterable of values to look through 55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 56 | * @param mixed|null $returnValue the value to return if nothing matches 57 | * 58 | * @see findEntry 59 | * @see findLastEntry 60 | * @see findIndex 61 | * @see findLast 62 | * @see findLastIndex 63 | * @see where 64 | * 65 | * @return mixed|null The entity from the iterable. If no value is found, `$returnValue` is returned, which defaults to 66 | * `null`. 67 | */ 68 | function find($collection, $condition, $returnValue = null) 69 | { 70 | $entry = \__::findEntry($collection, $condition); 71 | 72 | if ($entry === null) { 73 | return $returnValue; 74 | } 75 | 76 | return $entry[1]; 77 | } 78 | -------------------------------------------------------------------------------- /src/__/collections/concat.php: -------------------------------------------------------------------------------- 1 | ['favorite' => 'red', 5], 3], 18 | * [10, 'color' => ['favorite' => 'green', 'blue']] 19 | * ); 20 | * ``` 21 | * 22 | * **Result** 23 | * 24 | * ``` 25 | * [ 26 | * 'color' => ['favorite' => ['green'], 5, 'blue'], 27 | * 3, 28 | * 10 29 | * ] 30 | * ``` 31 | * 32 | * @since 0.2.0 added support for iterables 33 | * 34 | * @param iterable|\stdClass $collection Collection to assign to. 35 | * @param iterable|\stdClass ...$_ N other collections to assign. 36 | * 37 | * @return array|\stdClass If the first argument given to this function is an 38 | * `\stdClass`, an `\stdClass` will be returned. Otherwise, an array will be 39 | * returned. 40 | */ 41 | function concat($collection, $_) 42 | { 43 | $args = func_get_args(); 44 | $areArrayish = \__::every($args, function ($arg) { 45 | return \__::isArray($arg) || $arg instanceof \stdClass; 46 | }); 47 | 48 | if ($areArrayish) { 49 | $argsAsArrays = \__::map($args, function ($arg) { 50 | return (array)$arg; 51 | }); 52 | $merged = call_user_func_array('array_merge', $argsAsArrays); 53 | 54 | return ($collection instanceof \stdClass) ? (object)$merged : $merged; 55 | } 56 | 57 | if ($collection instanceof \Iterator || $collection instanceof \IteratorAggregate) { 58 | $result = iterator_to_array(\__::getIterator($collection)); 59 | } else { 60 | $result = (array)$collection; 61 | } 62 | 63 | foreach (\__::drop($args, 1) as $iterable) { 64 | foreach ($iterable as $key => $item) { 65 | if (\__::isNumber($key)) { 66 | $result[] = $item; 67 | } else { 68 | $result[$key] = $item; 69 | } 70 | } 71 | } 72 | 73 | return $result; 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | 7 | jobs: 8 | tests: 9 | name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-versions: 15 | - '5.5' 16 | - '5.6' 17 | - '7.0' 18 | - '7.1' 19 | - '7.2' 20 | - '7.3' 21 | - '7.4' 22 | - '8.0' 23 | - '8.1' 24 | os: [ubuntu-latest] 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | 30 | - name: Set up PHP ${{ matrix.php-versions }} 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php-versions }} 34 | extensions: mbstring 35 | coverage: pcov 36 | 37 | - name: Setup Problem Matchers for PHP 38 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 39 | 40 | - name: Setup Problem Matchers for PHPUnit 41 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 42 | 43 | - name: Validate composer.json and composer.lock 44 | run: composer validate 45 | 46 | - name: Get Composer Cache Directory 47 | id: composer-cache 48 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 49 | 50 | - name: Cache dependencies 51 | uses: actions/cache@v3 52 | with: 53 | path: ${{ steps.composer-cache.outputs.dir }} 54 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 55 | restore-keys: ${{ runner.os }}-composer- 56 | 57 | - name: Install dependencies 58 | run: | 59 | # Don't use our lock file for unit tests 60 | rm composer.lock 61 | 62 | # Remove PHP 8.1 requirement 63 | sed -i '/"php": ">=8\.1",/d' composer.json 64 | 65 | # Remove dependencies we don't need for unit tests that break platform requirements 66 | composer remove --dev \ 67 | bamarni/composer-bin-plugin 68 | 69 | composer install 70 | 71 | - name: Run test suite 72 | run: | 73 | composer test 74 | composer bench 75 | -------------------------------------------------------------------------------- /src/__/arrays/chunk.php: -------------------------------------------------------------------------------- 1 | $item) { 22 | if ($counter === $size) { 23 | yield $workspace; 24 | 25 | // Reset our workspace after we've yielded our last chunk 26 | $workspace = []; 27 | $counter = 0; 28 | } 29 | 30 | $key = $preserveKeys ? $key : $counter; 31 | $workspace[$key] = $item; 32 | ++$counter; 33 | } 34 | 35 | yield $workspace; 36 | } 37 | 38 | /** 39 | * Creates an array of elements split into groups the length of `$size`. 40 | * 41 | * If array can't be split evenly, the final chunk will be the remaining 42 | * elements. When `$preserveKeys` is set to TRUE, keys will be preserved. 43 | * Default is FALSE, which will reindex the chunk numerically. 44 | * 45 | * **Usage** 46 | * 47 | * ```php 48 | * __::chunk([1, 2, 3, 4, 5], 3); 49 | * ``` 50 | * 51 | * **Result** 52 | * 53 | * ``` 54 | * [[1, 2, 3], [4, 5]] 55 | * ``` 56 | * 57 | * @since 0.2.0 iterable objects are now supported 58 | * 59 | * @param iterable $iterable The original array 60 | * @param int $size The chunk size 61 | * @param bool $preserveKeys Whether or not to preserve index keys 62 | * 63 | * @throws \InvalidArgumentException when an non-array or non-traversable object is given for $iterable. 64 | * @throws \Exception when an `\IteratorAggregate` is given and `getIterator()` throws an exception. 65 | * 66 | * @return array|\Generator When given a `\Traversable` object for `$iterable`, a generator will be returned. 67 | * Otherwise, an array will be returned. 68 | */ 69 | function chunk($iterable, $size = 1, $preserveKeys = false) 70 | { 71 | if (is_array($iterable)) { 72 | return array_chunk($iterable, $size, $preserveKeys); 73 | } 74 | 75 | return chunkIterable($iterable, $size, $preserveKeys); 76 | } 77 | -------------------------------------------------------------------------------- /src/__/collections/get.php: -------------------------------------------------------------------------------- 1 | ['bar' => 'ter']], 'foo.bar'); 17 | * ``` 18 | * 19 | * **Result** 20 | * 21 | * ``` 22 | * 'ter' 23 | * ``` 24 | * 25 | * @param array|object $collection Array of values or object 26 | * @param string $path Array key or object attribute. Use a period 27 | * for depicting a new level in a multidimensional 28 | * array 29 | * @param mixed $default Default value to return if index not exist 30 | * 31 | * @return array|mixed|null 32 | */ 33 | function get($collection, $path, $default = null) 34 | { 35 | // TODO Make the algorithm recursive. 36 | // TODO Factorize between object and array access (use a $getter function, 37 | // as the $setter in __::set()). 38 | if (is_array($collection) && isset($collection[$path])) { 39 | return $collection[$path]; 40 | } 41 | 42 | foreach (\__::split($path, \__::DOT_NOTATION_DELIMITER) as $segment) { 43 | if (\is_object($collection) && !($collection instanceof \ArrayAccess)) { 44 | if (isset($collection->{$segment})) { 45 | $collection = $collection->{$segment}; 46 | } else { 47 | // TODO Remove Closure option: what is the point if it has no parameter: 48 | // it will always yield the same value? KISS. 49 | return $default && $default instanceof \Closure ? $default() : $default; 50 | } 51 | } else { 52 | if (isset($collection[$segment])) { 53 | $collection = $collection[$segment]; 54 | } else { 55 | // TODO Same as above on Closure. 56 | return $default && $default instanceof \Closure ? $default() : $default; 57 | } 58 | } 59 | } 60 | 61 | return $collection; 62 | } 63 | -------------------------------------------------------------------------------- /src/__/collections/concatDeep.php: -------------------------------------------------------------------------------- 1 | ['favorite' => 'red', 5], 3], 18 | * [10, 'color' => ['favorite' => 'green', 'blue']] 19 | * ); 20 | * ``` 21 | * 22 | * **Result** 23 | * 24 | * ``` 25 | * [ 26 | * 'color' => [ 27 | * 'favorite' => ['red', 'green'], 28 | * 5, 29 | * 'blue' 30 | * ], 31 | * 3, 32 | * 10 33 | * ] 34 | * ``` 35 | * 36 | * @since 0.2.0 iterable support was added 37 | * 38 | * @param iterable|\stdClass $collection First collection to concatDeep. 39 | * @param iterable|\stdClass ...$_ N other collections to concatDeep. 40 | * 41 | * @return array|\stdClass A concatenated collection. When the first argument given 42 | * is an `\stdClass`, then resulting value will be an `\stdClass`. Otherwise, 43 | * an array will always be returned. 44 | */ 45 | function concatDeep($collection, $_) 46 | { 47 | return \__::reduceRight(func_get_args(), function ($source, $result) { 48 | if ($result instanceof \Iterator || $result instanceof \IteratorAggregate) { 49 | $result = iterator_to_array(\__::getIterator($result)); 50 | } 51 | 52 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) { 53 | if (!\__::has($result, $key)) { 54 | $result = \__::set($result, $key, $sourceValue); 55 | } elseif (is_numeric($key)) { 56 | $result = \__::concat($result, [$sourceValue]); 57 | } else { 58 | $resultValue = \__::get($result, $key); 59 | $result = \__::set($result, $key, concatDeep( 60 | \__::isCollection($resultValue) ? $resultValue : (array)$resultValue, 61 | \__::isCollection($sourceValue) ? $sourceValue : (array)$sourceValue 62 | )); 63 | } 64 | }); 65 | return $result; 66 | }, []); 67 | } 68 | -------------------------------------------------------------------------------- /src/__/collections/findLast.php: -------------------------------------------------------------------------------- 1 | "trick", 16 | * "pen" => "defend", 17 | * "motherly" => "wide", 18 | * "may" => "needle", 19 | * "sweat" => "cake", 20 | * "sword" => "defend", 21 | * ]; 22 | * 23 | * __::findLast($data, "defend"); 24 | * ``` 25 | * 26 | * **Result** 27 | * 28 | * ``` 29 | * "defend" 30 | * ``` 31 | * 32 | * **Find by Callback in an Array** 33 | * 34 | * ```php 35 | * $data = [ 36 | * "table" => (object)["name" => "trick"], 37 | * "pen" => (object)["name" => "defend"], 38 | * "motherly" => (object)["name" => "wide"], 39 | * "may" => (object)["name" => "needle"], 40 | * "sweat" => (object)["name" => "cake"], 41 | * "sword" => (object)["name" => "defend"], 42 | * ]; 43 | * 44 | * __::findLast($data, static function ($object, $key, $collection) { 45 | * return $object->name === "defend"; 46 | * }); 47 | * ``` 48 | * 49 | * **Result** 50 | * 51 | * ``` 52 | * $data["sword"] 53 | * ``` 54 | * 55 | * @since 0.2.1 added to bottomline 56 | * 57 | * @param iterable $collection an array or iterable of values to look through 58 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 59 | * @param mixed|null $returnValue the value to return if nothing matches 60 | * 61 | * @see find 62 | * @see findEntry 63 | * @see findLastEntry 64 | * @see findIndex 65 | * @see findLastIndex 66 | * @see where 67 | * 68 | * @return mixed|null The entity from the iterable. If no value is found, `$returnValue` is returned, which defaults to 69 | * `null`. 70 | */ 71 | function findLast($collection, $condition, $returnValue = null) 72 | { 73 | return \__::find(\__::reverseIterable($collection), $condition, $returnValue); 74 | } 75 | -------------------------------------------------------------------------------- /tests/__/Collections/PickTest.php: -------------------------------------------------------------------------------- 1 | 1, 'b' => ['c' => 3, 'd' => 4], 'h' => 5]; 16 | 17 | // Act 18 | $x = __::pick($a, ['a', 'b.d', 'e', 'f.g']); 19 | 20 | // Assert 21 | $this->assertEquals([ 22 | 'a' => 1, 23 | 'b' => ['d' => 4], 24 | 'e' => null, 25 | 'f' => ['g' => null] 26 | ], $x); 27 | } 28 | 29 | public function testPickDefaults() 30 | { 31 | // Arrange. 32 | $a = ['nasa' => 1, 'cnsa' => 42]; 33 | $b = ['a' => 1, 'b' => ['c' => 3, 'd' => 4]]; 34 | 35 | // Act. 36 | $x = __::pick($a, ['cnsa', 'esa', 'jaxa'], 26); 37 | $y = __::pick($b, ['a', 'b.d', 'e', 'f.g'], 'default'); 38 | 39 | // Assert. 40 | $this->assertEquals([ 41 | 'cnsa' => 42, 42 | 'esa' => 26, 43 | 'jaxa' => 26, 44 | ], $x); 45 | $this->assertEquals([ 46 | 'a' => 1, 47 | 'b' => ['d' => 4], 48 | 'e' => 'default', 49 | 'f' => ['g' => 'default'] 50 | ], $y); 51 | } 52 | 53 | public function testPickObject() 54 | { 55 | // Arrange. 56 | $a = new \stdClass(); 57 | $a->paris = 10659489; 58 | $a->marseille = 1578484; 59 | $a->lyon = 1620331; 60 | 61 | // Act. 62 | $x = __::pick($a, ['marseille', 'london']); 63 | 64 | // Assert. 65 | $this->assertEquals((object)[ 66 | 'marseille' => 1578484, 67 | 'london' => null 68 | ], $x); 69 | } 70 | 71 | public function testPickIterable() 72 | { 73 | // Arrange 74 | $a = new ArrayIterator(['a' => 1, 'b' => ['c' => 3, 'd' => 4], 'h' => 5]); 75 | 76 | // Act 77 | $x = __::pick($a, ['a', 'b.d', 'e', 'f.g']); 78 | 79 | // Assert 80 | $this->assertEquals([ 81 | 'a' => 1, 82 | 'b' => ['d' => 4], 83 | 'e' => null, 84 | 'f' => ['g' => null] 85 | ], $x); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/__/Collections/PluckTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 'bis' => 'ter', '' => 0], 17 | ['foo' => 'bar2', 'bis' => 'ter2', '' => 1], 18 | ]; 19 | 20 | $b = new \stdClass(); 21 | $b->one = new \stdClass(); 22 | $b->one->foo = 'bar'; 23 | $b->two = new \stdClass(); 24 | $b->two->foo = 'bar2'; 25 | $b->three = new \stdClass(); 26 | $c = [$b->one, $b->two]; 27 | 28 | $d = [ 29 | ['foo' => ['bar' => ['baz' => 1]]], 30 | ['foo' => ['bar' => ['baz' => 2]]] 31 | ]; 32 | $e = new \stdClass(); 33 | $e->one = new \stdClass(); 34 | $e->one->foo = new \stdClass(); 35 | $e->one->foo->bar = ['baz' => 1]; 36 | $e->two = new \stdClass(); 37 | $e->two->foo = new \stdClass(); 38 | $e->two->foo->bar = new \stdClass(); 39 | $e->two->foo->bar->baz = 2; 40 | 41 | // Act 42 | $x = __::pluck($a, 'foo'); 43 | $x2 = __::pluck($a, ''); 44 | 45 | $y = __::pluck($b, 'foo'); 46 | $y2 = __::pluck($c, 'foo'); 47 | 48 | $z = __::pluck($d, 'foo.bar.baz'); 49 | $z2 = __::pluck($e, 'foo.bar.baz'); 50 | 51 | // Assert 52 | $this->assertEquals(['bar', 'bar2'], $x); 53 | $this->assertEquals([0, 1], $x2); 54 | 55 | $this->assertEquals(['bar', 'bar2', null], $y); 56 | $this->assertEquals(['bar', 'bar2'], $y2); 57 | 58 | $this->assertEquals([1, 2], $z); 59 | $this->assertEquals([1, 2], $z2); 60 | } 61 | 62 | public function testPluckIterable() 63 | { 64 | // Arrange 65 | $a = new ArrayIterator([ 66 | ['foo' => 'bar', 'bis' => 'ter', '' => 0], 67 | ['foo' => 'bar2', 'bis' => 'ter2', '' => 1], 68 | ]); 69 | 70 | // Act 71 | $x = __::pluck($a, 'foo'); 72 | $x2 = __::pluck($a, ''); 73 | 74 | // Assert 75 | $this->assertEquals(['bar', 'bar2'], $x); 76 | $this->assertEquals([0, 1], $x2); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/__/collections/findEntry.php: -------------------------------------------------------------------------------- 1 | "trick", 13 | * "pen" => "defend", 14 | * "motherly" => "wide", 15 | * "may" => "needle", 16 | * "sweat" => "cake", 17 | * "sword" => "defend", 18 | * ]; 19 | * 20 | * __::findEntry($data, "defend"); 21 | * ``` 22 | * 23 | * **Result** 24 | * 25 | * ``` 26 | * ["pen", "defend"] 27 | * ``` 28 | * 29 | * **Find by Callback in an Array** 30 | * 31 | * ```php 32 | * $data = [ 33 | * "table" => (object)["name" => "trick"], 34 | * "pen" => (object)["name" => "defend"], 35 | * "motherly" => (object)["name" => "wide"], 36 | * "may" => (object)["name" => "needle"], 37 | * "sweat" => (object)["name" => "cake"], 38 | * "sword" => (object)["name" => "defend"], 39 | * ]; 40 | * 41 | * __::findEntry($data, static function ($object, $key, $collection) { 42 | * return $object->name === "defend"; 43 | * }); 44 | * ``` 45 | * 46 | * **Result** 47 | * 48 | * ``` 49 | * ["pen", $data["pen"]] 50 | * ``` 51 | * 52 | * @since 0.2.1 added to bottomline 53 | * 54 | * @param iterable $collection an array or iterable of values to look through 55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 56 | * 57 | * @see find 58 | * @see findLastEntry 59 | * @see findLastIndex 60 | * @see findIndex 61 | * @see findLast 62 | * @see where 63 | * 64 | * @return array|null An array with two values, the 0th index is the key and the 1st index is the value. Null is 65 | * returned if no entries can be found in the given collection. 66 | */ 67 | function findEntry($collection, $condition) 68 | { 69 | $comparison = is_callable($condition) ? $condition : static function ($value, $_key, $_arr) use ($condition) { 70 | return $value === $condition; 71 | }; 72 | 73 | foreach ($collection as $key => $arrItem) { 74 | if ($comparison($arrItem, $key, $collection)) { 75 | return [$key, $arrItem]; 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | -------------------------------------------------------------------------------- /tests/__/Collections/SizeTest.php: -------------------------------------------------------------------------------- 1 | [], 17 | 'expected' => 0, 18 | ], 19 | [ 20 | 'source' => [1, 2, 3], 21 | 'expected' => 3, 22 | ], 23 | [ 24 | 'source' => new \stdClass(), 25 | 'expected' => 0, 26 | ], 27 | [ 28 | 'source' => (object)['hello' => 'world'], 29 | 'expected' => 1, 30 | ], 31 | [ 32 | 'source' => false, 33 | 'expected' => false, 34 | ], 35 | [ 36 | 'source' => null, 37 | 'expected' => false, 38 | ], 39 | [ 40 | 'source' => 3, 41 | 'expected' => false, 42 | ], 43 | [ 44 | 'source' => new ArrayIterator([]), 45 | 'expected' => 0, 46 | ], 47 | [ 48 | 'source' => new ArrayIterator([1, 2, 3]), 49 | 'expected' => 3, 50 | ], 51 | [ 52 | 'source' => new MockIteratorAggregate([]), 53 | 'expected' => 0, 54 | ], 55 | [ 56 | 'source' => new MockIteratorAggregate([1, 2, 3]), 57 | 'expected' => 3, 58 | ], 59 | [ 60 | 'source' => call_user_func(function () { 61 | yield 1; 62 | }), 63 | 'expected' => 1, 64 | ], 65 | [ 66 | 'source' => call_user_func(function () { 67 | }), 68 | 'expected' => 0, 69 | ], 70 | ]; 71 | } 72 | 73 | /** 74 | * @dataProvider provideSizeCases 75 | * 76 | * @param mixed $source 77 | * @param int $expected 78 | */ 79 | public function testSize($source, $expected) 80 | { 81 | $this->assertEquals($expected, __::size($source)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/__/Arrays/DropTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([2, 3], $x); 25 | $this->assertEquals([3], $y); 26 | $this->assertEquals([], $z); 27 | $this->assertEquals([1, 2, 3], $xa); 28 | } 29 | 30 | public function testDropWithIterator() 31 | { 32 | $a = [1, 2, 3, 4, 5]; 33 | $aItr = new ArrayIterator($a); 34 | 35 | $expected = __::drop($a, 3); 36 | $actual = __::drop($aItr, 3); 37 | $itrSize = 0; 38 | 39 | foreach ($actual as $i => $item) { 40 | ++$itrSize; 41 | $this->assertEquals($item, $expected[$i]); 42 | } 43 | 44 | $this->assertEquals(count($expected), $itrSize); 45 | } 46 | 47 | public function testDropWithIteratorAggregate() 48 | { 49 | $a = [1, 2, 3, 4, 5]; 50 | $aItrAgg = new MockIteratorAggregate($a); 51 | 52 | $expected = __::drop($a, 3); 53 | $actual = __::drop($aItrAgg, 3); 54 | $itrSize = 0; 55 | 56 | foreach ($actual as $i => $item) { 57 | ++$itrSize; 58 | $this->assertEquals($item, $expected[$i]); 59 | } 60 | 61 | $this->assertEquals(count($expected), $itrSize); 62 | } 63 | 64 | public function testDropWithGenerator() 65 | { 66 | $a = [1, 2, 3, 4, 5]; 67 | $generator = call_user_func(function () use ($a) { 68 | foreach ($a as $item) { 69 | yield $item; 70 | } 71 | }); 72 | 73 | $this->assertInstanceOf(\Generator::class, $generator); 74 | 75 | $expected = __::drop($a, 3); 76 | $actual = __::drop($generator, 3); 77 | $itrSize = 0; 78 | 79 | foreach ($actual as $i => $item) { 80 | ++$itrSize; 81 | $this->assertEquals($item, $expected[$i]); 82 | } 83 | 84 | $this->assertEquals(count($expected), $itrSize); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/__/Collections/IsEmptyTest.php: -------------------------------------------------------------------------------- 1 | [], 18 | 'expected' => true, 19 | ], 20 | [ 21 | 'source' => ['Falcon', 'Heavy'], 22 | 'expected' => false, 23 | ], 24 | [ 25 | 'source' => new \stdClass(), 26 | 'expected' => true, 27 | ], 28 | [ 29 | 'source' => (object)['Baie' => 'Goji'], 30 | 'expected' => false, 31 | ], 32 | 33 | // Assert non-collections 34 | [ 35 | 'source' => null, 36 | 'expected' => true, 37 | ], 38 | [ 39 | 'source' => 3, 40 | 'expected' => true, 41 | ], 42 | [ 43 | 'source' => true, 44 | 'expected' => true, 45 | ], 46 | 47 | 48 | // Assert iterators 49 | [ 50 | 'source' => new ArrayIterator([]), 51 | 'expected' => true, 52 | ], 53 | [ 54 | 'source' => new MockIteratorAggregate([]), 55 | 'expected' => true, 56 | ], 57 | [ 58 | 'source' => new ArrayIterator([1, 2]), 59 | 'expected' => false, 60 | ], 61 | [ 62 | 'source' => new MockIteratorAggregate([1, 2]), 63 | 'expected' => false, 64 | ], 65 | [ 66 | 'source' => call_user_func(function () { 67 | yield 1; 68 | yield 5; 69 | }), 70 | 'expected' => false, 71 | ], 72 | ]; 73 | } 74 | 75 | /** 76 | * @dataProvider provideIsEmptyCases 77 | * 78 | * @param mixed $source 79 | * @param bool $expected 80 | */ 81 | public function testIsEmpty($source, $expected) 82 | { 83 | $this->assertEquals($expected, __::isEmpty($source)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/__/Arrays/DropRightTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([1, 2], $out1); 22 | $this->assertEquals([1], $out2); 23 | $this->assertEquals([], $out3); 24 | $this->assertEquals([1, 2, 3], $out4); 25 | } 26 | 27 | public function testDropRightWithIterator() 28 | { 29 | $a = [1, 2, 3, 4, 5]; 30 | $aItr = new ArrayIterator($a); 31 | 32 | $expected = __::dropRight($a, 3); 33 | $actual = __::dropRight($aItr, 3); 34 | $itrSize = 0; 35 | 36 | foreach ($actual as $i => $item) { 37 | ++$itrSize; 38 | $this->assertEquals($item, $expected[$i]); 39 | } 40 | 41 | $this->assertEquals(count($expected), $itrSize); 42 | } 43 | 44 | public function testDropRightWithIteratorAggregate() 45 | { 46 | $a = [1, 2, 3, 4, 5]; 47 | $aItrAgg = new MockIteratorAggregate($a); 48 | 49 | $expected = __::dropRight($a, 3); 50 | $actual = __::dropRight($aItrAgg, 3); 51 | $itrSize = 0; 52 | 53 | foreach ($actual as $i => $item) { 54 | ++$itrSize; 55 | $this->assertEquals($item, $expected[$i]); 56 | } 57 | 58 | $this->assertEquals(count($expected), $itrSize); 59 | } 60 | 61 | public function testDropRightWithGenerator() 62 | { 63 | $a = [1, 2, 3, 4, 5]; 64 | $generator = call_user_func(function () use ($a) { 65 | foreach ($a as $item) { 66 | yield $item; 67 | } 68 | }); 69 | 70 | $this->assertInstanceOf(\Generator::class, $generator); 71 | 72 | $expected = __::dropRight($a, 3); 73 | $actual = __::dropRight($generator, 3); 74 | $itrSize = 0; 75 | 76 | foreach ($actual as $i => $item) { 77 | ++$itrSize; 78 | $this->assertEquals($item, $expected[$i]); 79 | } 80 | 81 | $this->assertEquals(count($expected), $itrSize); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/__/Collections/MergeTest.php: -------------------------------------------------------------------------------- 1 | ['favorite' => 'red', 'model' => 3, 5], 3]; 16 | $a2 = [10, 'color' => ['favorite' => 'green', 'blue']]; 17 | $b1 = ['a' => 0]; 18 | $b2 = ['a' => 1, 'b' => 2]; 19 | $b3 = ['c' => 3, 'd' => 4]; 20 | 21 | // Act 22 | $x = __::merge($a1, $a2); 23 | $y = __::merge($b1, $b2, $b3); 24 | 25 | // Assert 26 | $this->assertEquals(['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10], $x); 27 | $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $y); 28 | } 29 | 30 | public function testMergeObject() 31 | { 32 | // Arrange 33 | $a1 = (object)['color' => (object)['favorite' => 'red', 'model' => 3, 5]]; 34 | $a2 = (object)[10, 'color' => (object)['favorite' => 'green', 'blue']]; 35 | $b1 = (object)['a' => 0]; 36 | $b2 = (object)['a' => 1, 'b' => 2, 5]; 37 | $b3 = (object)['c' => 3, 'd' => 4, 6]; 38 | 39 | // Act 40 | $x = __::merge($a1, $a2); 41 | $y = __::merge($b1, $b2, $b3); 42 | 43 | // Assert 44 | $this->assertEquals((object)['color' => (object)['favorite' => 'green', 'model' => 3, 'blue'], 10], $x); 45 | $this->assertEquals((object)['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 6], $y); 46 | } 47 | 48 | public function testMergeIterable() 49 | { 50 | // Arrange 51 | $a1 = new ArrayIterator(['color' => ['favorite' => 'red', 'model' => 3, 5], 3]); 52 | $a2 = new ArrayIterator([10, 'color' => ['favorite' => 'green', 'blue']]); 53 | 54 | // Act 55 | $x = __::merge($a1, $a2); 56 | 57 | // Assert 58 | // Check we got back an array. 59 | $this->assertTrue(is_array($x)); 60 | $xValues = []; 61 | foreach ($x as $key => $value) { 62 | $xValues[$key] = $value; 63 | } 64 | $this->assertEquals(new ArrayIterator(['color' => ['favorite' => 'red', 'model' => 3, 5], 3]), $a1); 65 | $this->assertEquals(new ArrayIterator([10, 'color' => ['favorite' => 'green', 'blue']]), $a2); 66 | $this->assertEquals(['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10], $xValues); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maciejczyzewski/bottomline", 3 | "description": "A full-on PHP manipulation utility belt that provides support for working with arrays, objects, and iterables; a lodash or underscore equivalent for PHP.", 4 | "keywords": ["__", "bottomline", "library", "utility", "functions", "underscore", "lodash"], 5 | "license": "MIT", 6 | "type": "library", 7 | "authors": [ 8 | { 9 | "name": "Maciej A. Czyzewski", 10 | "email": "maciejanthonyczyzewski@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.5.0", 15 | "ext-mbstring": "*" 16 | }, 17 | "require-dev": { 18 | "php": ">=8.1", 19 | "ext-json": "*", 20 | "bamarni/composer-bin-plugin": "^1.8", 21 | "doctrine/instantiator": "^1.0||^1.1||^1.4", 22 | "erusev/parsedown": "^1.7", 23 | "nikic/php-parser": "^3.1||^4.13", 24 | "phpdocumentor/reflection-docblock": "^3.0||^4.3||^5.2", 25 | "phpunit/phpunit": "^4.8||^6.5||^9.5" 26 | }, 27 | "autoload": { 28 | "classmap": ["bottomline.php"], 29 | "psr-4": {"__\\": "src/__/"} 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "__\\DocGen\\": "docgen/", 34 | "__\\Test\\": "tests/__/", 35 | "__\\TestHelpers\\": "tests/Helpers/" 36 | } 37 | }, 38 | "scripts": { 39 | "post-install-cmd": [ 40 | "@composer bin php-cs-fixer install --ansi" 41 | ], 42 | "bench": "php tools/bench.php", 43 | "bin": "echo 'bin not installed'", 44 | "cs-fix": "php-cs-fixer fix -v", 45 | "cs-check": "php-cs-fixer fix --dry-run -v --diff", 46 | "doc": [ 47 | "php tools/phpDocGen.php", 48 | "@cs-fix" 49 | ], 50 | "site": [ 51 | "cd docs; bundle info github-pages > /dev/null 2>&1 || bundle install", 52 | "cd docs; bundle exec jekyll serve" 53 | ], 54 | "test": [ 55 | "# Silence PHP deprecation warnings before PHPUnit is run", 56 | "php -d error_reporting=24575 ./vendor/bin/phpunit" 57 | ] 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "bamarni/composer-bin-plugin": true 63 | } 64 | }, 65 | "extra": { 66 | "branch-alias": { 67 | "dev-master": "0.2-dev" 68 | }, 69 | "bamarni-bin": { 70 | "bin-links": true, 71 | "forward-command": true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/__/Collections/MapTest.php: -------------------------------------------------------------------------------- 1 | [1, 2, 3], 17 | 'closure' => function ($n) { 18 | return $n * 3; 19 | }, 20 | 'isGenerator' => false, 21 | 'expected' => [3, 6, 9], 22 | ], 23 | [ 24 | 'source' => (object)['a' => 1, 'b' => 2, 'c' => 3], 25 | 'closure' => function ($n, $key) { 26 | return $key === 'c' ? $n : $n * 3; 27 | }, 28 | 'isGenerator' => false, 29 | 'expected' => [3, 6, 3], 30 | ], 31 | [ 32 | 'source' => new ArrayIterator([1, 2, 3]), 33 | 'closure' => function ($n) { 34 | return $n * 3; 35 | }, 36 | 'isGenerator' => true, 37 | 'expected' => [3, 6, 9], 38 | ], 39 | [ 40 | 'source' => new MockIteratorAggregate([1, 2, 3]), 41 | 'closure' => function ($n) { 42 | return $n * 6; 43 | }, 44 | 'isGenerator' => true, 45 | 'expected' => [6, 12, 18], 46 | ], 47 | [ 48 | 'source' => call_user_func(function () { 49 | yield 1; 50 | yield 2; 51 | yield 3; 52 | }), 53 | 'closure' => function ($n) { 54 | return $n * 3; 55 | }, 56 | 'isGenerator' => true, 57 | 'expected' => [3, 6, 9], 58 | ], 59 | ]; 60 | } 61 | 62 | /** 63 | * @dataProvider provideMapCases 64 | * 65 | * @param iterable $source 66 | * @param \Closure $closure 67 | * @param bool $isGenerator 68 | * @param array $expected 69 | */ 70 | public function testMap($source, $closure, $isGenerator, $expected) 71 | { 72 | $actual = __::map($source, $closure); 73 | 74 | if ($isGenerator) { 75 | $this->assertInstanceOf(\Generator::class, $actual); 76 | } else { 77 | $this->assertTrue(is_array($actual)); 78 | } 79 | 80 | foreach ($actual as $key => $value) { 81 | $this->assertEquals($expected[$key], $value); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/__/Collections/SetTest.php: -------------------------------------------------------------------------------- 1 | ['bar' => 'ter']]; 17 | 18 | // Act 19 | $x = __::set($a, 'foo.baz.ber', 'fer'); 20 | $y = __::set($a, 'foo.bar', 'fer2'); 21 | 22 | // Assert 23 | $this->assertEquals(['foo' => ['bar' => 'ter']], $a); 24 | $this->assertEquals(['ber' => 'fer'], $x['foo']['baz']); 25 | $this->assertEquals(['foo' => ['bar' => 'fer2']], $y); 26 | } 27 | 28 | public function testSetArrayAccess() 29 | { 30 | $aa = new ArrayAccessible(); 31 | 32 | __::set($aa, 'foo.ubi', [ 33 | 'bar' => 'qaz', 34 | ]); 35 | __::set($aa, 'faa.raot.uft', 100); 36 | 37 | $this->assertTrue(is_array(__::get($aa, 'foo'))); 38 | $this->assertTrue(is_array(__::get($aa, 'faa'))); 39 | $this->assertTrue(is_array(__::get($aa, 'faa.raot'))); 40 | 41 | $this->assertEquals('qaz', __::get($aa, 'foo.ubi.bar')); 42 | $this->assertEquals(42, __::get($aa, 'foo.nonexistent', 42)); 43 | } 44 | 45 | public function testSetObject() 46 | { 47 | // Arrange. 48 | $a = (object)['foo' => (object)['bar' => 'ter']]; 49 | 50 | // Act. 51 | $x = __::set($a, 'foo.baz.ber', 'fer'); 52 | $y = __::set($a, 'foo.bar', 'fer2'); 53 | 54 | // Assert. 55 | $this->assertEquals((object )['foo' => (object)['bar' => 'ter']], $a); 56 | $this->assertEquals((object)['ber' => 'fer'], $x->foo->baz); 57 | $this->assertEquals((object)['foo' => (object)['bar' => 'fer2']], $y); 58 | } 59 | 60 | public function testSetOverride() 61 | { 62 | // Arrange 63 | $a = ['foo' => ['bar' => 'ter']]; 64 | 65 | // Act 66 | $x = __::set($a, 'foo.bar.not_exist', 'baz'); 67 | 68 | // Assert. 69 | $this->assertEquals(['foo' => ['bar' => 'ter']], $a); 70 | $this->assertEquals(['foo' => ['bar' => ['not_exist' => 'baz']]], $x); 71 | } 72 | 73 | public function testSetIterable() 74 | { 75 | // Arrange 76 | $a = new ArrayIterator(['foo' => ['bar' => 'ter']]); 77 | 78 | // Act 79 | $x = __::set($a, 'foo.baz.ber', 'fer'); 80 | 81 | // Assert 82 | $this->assertEquals(new ArrayIterator(['foo' => ['bar' => 'ter']]), $a); 83 | $this->assertEquals(['ber' => 'fer'], $x['foo']['baz']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/__/Collections/GetTest.php: -------------------------------------------------------------------------------- 1 | ['bar' => 'ter'], 17 | 'baz' => ['foo' => ['obj' => $o]] 18 | ]; 19 | 20 | // Act 21 | $x = __::get($a, 'foo.bar'); 22 | $x2 = __::get($a, 'foo.bar', 'default'); 23 | $y = __::get($a, 'foo.baz'); 24 | $y2 = __::get($a, 'foo.baz', 'default'); 25 | $y3 = __::get($a, 'foo.baz', function () { 26 | return 'default_from_callback'; 27 | }); 28 | $z = __::get($a, 'baz.foo.obj'); 29 | 30 | // Assert 31 | $this->assertEquals('ter', $x); 32 | $this->assertEquals('ter', $x2); 33 | $this->assertNull($y); 34 | $this->assertEquals('default', $y2); 35 | $this->assertEquals('default_from_callback', $y3); 36 | $this->assertEquals($o, $z); 37 | } 38 | 39 | public function testGetArrayAccess() 40 | { 41 | $aa = new ArrayAccessible(); 42 | $aa['foo'] = [ 43 | 'bar' => 'quim', 44 | ]; 45 | $aa['bar'] = 5; 46 | $aa['caz'] = new \stdClass(); 47 | $aa['caz']->daer = 'heft'; 48 | 49 | $this->assertEquals('quim', __::get($aa, 'foo.bar')); 50 | $this->assertEquals(5, __::get($aa, 'bar')); 51 | $this->assertEquals('heft', __::get($aa, 'caz.daer')); 52 | 53 | $this->assertNull(__::get($aa, 'foo.cat')); 54 | } 55 | 56 | public function testGetObjects() 57 | { 58 | // Arrange 59 | $o = new \stdClass(); 60 | $a = new \stdClass(); 61 | $a->foo = new \stdClass(); 62 | $a->foo->bar = 'ter'; 63 | $a->baz = new \stdClass(); 64 | $a->baz->foo = new \stdClass(); 65 | $a->baz->foo->obj = $o; 66 | 67 | // Act 68 | $x = __::get($a, 'foo.bar'); 69 | $x2 = __::get($a, 'foo.bar', 'default'); 70 | $y = __::get($a, 'foo.baz'); 71 | $y2 = __::get($a, 'foo.baz', 'default'); 72 | $y3 = __::get($a, 'foo.baz', function () { 73 | return 'default_from_callback'; 74 | }); 75 | $z = __::get($a, 'baz.foo.obj'); 76 | 77 | // Assert 78 | $this->assertEquals('ter', $x); 79 | $this->assertEquals('ter', $x2); 80 | $this->assertNull($y); 81 | $this->assertEquals('default', $y2); 82 | $this->assertEquals('default_from_callback', $y3); 83 | $this->assertEquals($o, $z); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/__/Arrays/DropRightWhileTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([1, 2], $out1); 19 | $this->assertEquals([1, 2], $out2); 20 | $this->assertEquals([], $out3); 21 | } 22 | 23 | public function testDropRightWhileWithCallback() 24 | { 25 | $f = static function ($item) { 26 | return $item >= 3; 27 | }; 28 | 29 | $out1 = __::dropRightWhile([1, 2, 3, 4], $f); 30 | $out2 = __::dropRightWhile([1, 2], $f); 31 | $out3 = __::dropRightWhile([], $f); 32 | 33 | $this->assertEquals([1, 2], $out1); 34 | $this->assertEquals([1, 2], $out2); 35 | $this->assertEquals([], $out3); 36 | } 37 | 38 | public function testDropRightWhileWithIterator() 39 | { 40 | $a = [1, 2, 3, 4, 5]; 41 | $aItr = new ArrayIterator($a); 42 | 43 | $expected = __::dropRightWhile($a, 3); 44 | $actual = __::dropRightWhile($aItr, 3); 45 | $itrSize = 0; 46 | 47 | foreach ($actual as $i => $item) { 48 | ++$itrSize; 49 | $this->assertEquals($item, $expected[$i]); 50 | } 51 | 52 | $this->assertEquals(count($expected), $itrSize); 53 | } 54 | 55 | public function testDropRightWhileWithIteratorAggregate() 56 | { 57 | $a = [1, 2, 3, 4, 5]; 58 | $aItrAgg = new MockIteratorAggregate($a); 59 | 60 | $expected = __::dropRightWhile($a, 3); 61 | $actual = __::dropRightWhile($aItrAgg, 3); 62 | $itrSize = 0; 63 | 64 | foreach ($actual as $i => $item) { 65 | ++$itrSize; 66 | $this->assertEquals($item, $expected[$i]); 67 | } 68 | 69 | $this->assertEquals(count($expected), $itrSize); 70 | } 71 | 72 | public function testDropRightWhileWithGenerator() 73 | { 74 | $a = [1, 2, 3, 4, 5]; 75 | $generator = call_user_func(function () use ($a) { 76 | foreach ($a as $item) { 77 | yield $item; 78 | } 79 | }); 80 | 81 | $this->assertInstanceOf(\Generator::class, $generator); 82 | 83 | $expected = __::dropRightWhile($a, 3); 84 | $actual = __::dropRightWhile($generator, 3); 85 | $itrSize = 0; 86 | 87 | foreach ($actual as $i => $item) { 88 | ++$itrSize; 89 | $this->assertEquals($item, $expected[$i]); 90 | } 91 | 92 | $this->assertEquals(count($expected), $itrSize); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/__/collections/findIndex.php: -------------------------------------------------------------------------------- 1 | "trick", 35 | * "pen" => "defend", 36 | * "motherly" => "wide", 37 | * "may" => "needle", 38 | * "sweat" => "cake", 39 | * "sword" => "defend", 40 | * ]; 41 | * 42 | * __::findIndex($data, "defend") 43 | * ``` 44 | * 45 | * **Result** 46 | * 47 | * ``` 48 | * "pen" 49 | * ``` 50 | * 51 | * **Find by Callback Usage** 52 | * 53 | * ```php 54 | * $data = [ 55 | * "table" => (object)["name" => "trick"], 56 | * "pen" => (object)["name" => "defend"], 57 | * "motherly" => (object)["name" => "wide"], 58 | * "may" => (object)["name" => "needle"], 59 | * "sweat" => (object)["name" => "cake"], 60 | * "sword" => (object)["name" => "defend"], 61 | * ]; 62 | * 63 | * __::findIndex($data, static function ($object, $key, $collection) { 64 | * return $object->name === "defend"; 65 | * }) 66 | * ``` 67 | * 68 | * **Result** 69 | * 70 | * ``` 71 | * "pen" 72 | * ``` 73 | * 74 | * @since 0.2.1 added to bottomline 75 | * 76 | * @param iterable $collection an array or iterable of values to look through 77 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 78 | * @param int|string $returnValue the value to return if nothing matches 79 | * 80 | * @see find 81 | * @see findEntry 82 | * @see findLastEntry 83 | * @see findLast 84 | * @see findLastIndex 85 | * @see where 86 | * 87 | * @return int|string The index where the respective value is found. When given a numerically 88 | * indexed array, an int will be returned but when an associative array is given, a string will 89 | * be returned. 90 | * If no value is found, `$returnValue` is returned, which defaults to `-1`. 91 | */ 92 | function findIndex($collection, $condition, $returnValue = -1) 93 | { 94 | $entry = \__::findEntry($collection, $condition); 95 | 96 | if ($entry === null) { 97 | return $returnValue; 98 | } 99 | 100 | return $entry[0]; 101 | } 102 | -------------------------------------------------------------------------------- /src/__/collections/findLastIndex.php: -------------------------------------------------------------------------------- 1 | "trick", 38 | * "pen" => "defend", 39 | * "motherly" => "wide", 40 | * "may" => "needle", 41 | * "sweat" => "cake", 42 | * "sword" => "defend", 43 | * ]; 44 | * 45 | * __::findLastIndex($data, "defend") 46 | * ``` 47 | * 48 | * **Result** 49 | * 50 | * ``` 51 | * "sword" 52 | * ``` 53 | * 54 | * **Find by Callback Usage** 55 | * 56 | * ```php 57 | * $data = [ 58 | * "table" => (object)["name" => "trick"], 59 | * "pen" => (object)["name" => "defend"], 60 | * "motherly" => (object)["name" => "wide"], 61 | * "may" => (object)["name" => "needle"], 62 | * "sweat" => (object)["name" => "cake"], 63 | * "sword" => (object)["name" => "defend"], 64 | * ]; 65 | * 66 | * __::findLastIndex($data, static function ($object, $key, $collection) { 67 | * return $object->name === "defend"; 68 | * }) 69 | * ``` 70 | * 71 | * **Result** 72 | * 73 | * ``` 74 | * "sword" 75 | * ``` 76 | * 77 | * @since 0.2.1 added to bottomline 78 | * 79 | * @param iterable $collection an array or iterable of values to look through 80 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback 81 | * @param int|string $returnValue the value to return if nothing matches 82 | * 83 | * @see find 84 | * @see findEntry 85 | * @see findLastEntry 86 | * @see findIndex 87 | * @see findLast 88 | * @see where 89 | * 90 | * @return int|string The index where the respective value is found. When given a numerically 91 | * indexed array, an int will be returned but when an associative array is given, a string will 92 | * be returned. 93 | * If no value is found, `$returnValue` is returned, which defaults to `-1`. 94 | */ 95 | function findLastIndex($collection, $condition, $returnValue = -1) 96 | { 97 | return \__::findIndex(\__::reverseIterable($collection), $condition, $returnValue); 98 | } 99 | -------------------------------------------------------------------------------- /src/__/arrays/dropWhile.php: -------------------------------------------------------------------------------- 1 | name = $documentedParam->getVariableName(); 28 | $this->description = $documentedParam->getDescription()->render(); 29 | $this->isVariadic = $documentedParam->isVariadic(); 30 | $this->type = $documentedParam->getType(); 31 | $this->defaultValue = null; 32 | $this->defaultValueAsString = null; 33 | 34 | if ($reflectedParam !== null && $reflectedParam->isOptional()) { 35 | try { 36 | $defaultValue = $reflectedParam->getDefaultValue(); 37 | $this->defaultValue = $defaultValue; 38 | if ($defaultValue === null) { 39 | $this->defaultValueAsString = 'null'; 40 | } elseif (is_bool($defaultValue)) { 41 | $this->defaultValueAsString = $defaultValue ? 'true' : 'false'; 42 | } elseif (is_string($defaultValue)) { 43 | $this->defaultValueAsString = sprintf("'%s'", $defaultValue); 44 | } elseif (is_array($defaultValue)) { 45 | $this->defaultValueAsString = '[]'; 46 | } else { 47 | $this->defaultValueAsString = $defaultValue; 48 | } 49 | } catch (Exception $e) { 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * @see Method 56 | */ 57 | public function getMethodArgumentDefinition(): array 58 | { 59 | return [ 60 | 'name' => $this->getSignature(), 61 | 'type' => $this->type, 62 | ]; 63 | } 64 | 65 | public function getSignature(): string 66 | { 67 | if ($this->defaultValueAsString) { 68 | return "{$this->name} = {$this->defaultValueAsString}"; 69 | } 70 | if ($this->isVariadic) { 71 | return "{$this->name},..."; 72 | } 73 | 74 | return $this->name; 75 | } 76 | 77 | public function jsonSerialize(): mixed 78 | { 79 | return [ 80 | 'name' => $this->name, 81 | 'isVariadic' => $this->isVariadic, 82 | 'description' => $this->description, 83 | 'defaultValue' => $this->defaultValue, 84 | 'defaultValueAsString' => $this->defaultValueAsString, 85 | 'type' => (string)$this->type, 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/__/arrays/dropRightWhile.php: -------------------------------------------------------------------------------- 1 | = 0; $i--) { 38 | if ($comparison($input[$i])) { 39 | continue; 40 | } 41 | 42 | return array_slice($input, 0, $i + 1); 43 | } 44 | 45 | return []; 46 | } 47 | 48 | /** 49 | * Creates a slice of the provided array with all elements matching the condition 50 | * removed from the end. 51 | * 52 | * If the provided iterator is an array, then an array will be returned. 53 | * Otherwise, a Generator will be returned. In this latter case, the entire 54 | * iterable will be buffered in memory. If the iterable contains many 55 | * elements, then this could cause memory issues. 56 | * 57 | * **Drop by Primitive Condition** 58 | * 59 | * ```php 60 | * __::dropRightWhile([1, 2, 3, 3], 3); 61 | * ``` 62 | * 63 | * **Result** 64 | * 65 | * ```php 66 | * [1, 2] 67 | * ``` 68 | * 69 | * **Drop by Callback** 70 | * 71 | * ```php 72 | * __::dropRightWhile([1, 2, 3, 4, 5], static function ($item) { 73 | * return $item > 3; 74 | * }); 75 | * ``` 76 | * 77 | * **Result** 78 | * 79 | * ```php 80 | * [1, 2, 3] 81 | * ``` 82 | * 83 | * @param iterable $input An array of values. 84 | * @param \Closure|double|int|string|bool $condition Condition to drop by using either a primitive value or a callback 85 | * 86 | * @see drop 87 | * @see dropRight 88 | * @see dropRightWhile 89 | * 90 | * @since 0.2.3 added to Bottomline 91 | * 92 | * @return array|\Generator An array containing a subset of the input array with front items matching the condition 93 | * removed. If the provided iterable is not an array, then a Generator will be returned. 94 | */ 95 | function dropRightWhile($input, $condition) 96 | { 97 | $comparison = is_callable($condition) 98 | ? $condition 99 | : static function ($item) use ($condition) { 100 | return $item === $condition; 101 | }; 102 | 103 | if (is_array($input)) { 104 | return dropArrayRightWhile($input, $comparison); 105 | } 106 | 107 | return dropIteratorRightWhile($input, $comparison); 108 | } 109 | -------------------------------------------------------------------------------- /src/__/collections/reduce.php: -------------------------------------------------------------------------------- 1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'], 32 | * ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'Manhole'], 33 | * ['state' => 'IN', 'city' => 'Plainfield', 'object' => 'Basketball'], 34 | * ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'], 35 | * ['state' => 'CA', 'city' => 'Mountain View', 'object' => 'Space pen'], 36 | * ]; 37 | * 38 | * $iteratee = function ($accumulator, $value) { 39 | * if (isset($accumulator[$value['city']])) 40 | * $accumulator[$value['city']]++; 41 | * else 42 | * $accumulator[$value['city']] = 1; 43 | * return $accumulator; 44 | * }; 45 | * 46 | * __::reduce($c, $iteratee, []); 47 | * ``` 48 | * 49 | * **Result** 50 | * 51 | * ``` 52 | * [ 53 | * 'Indianapolis' => 2, 54 | * 'Plainfield' => 1, 55 | * 'San Diego' => 1, 56 | * 'Mountain View' => 1, 57 | * ] 58 | * ``` 59 | * 60 | * **Usage: Objects** 61 | * 62 | * ```php 63 | * $object = new \stdClass(); 64 | * $object->a = 1; 65 | * $object->b = 2; 66 | * $object->c = 1; 67 | * 68 | * __::reduce($object, function ($result, $value, $key) { 69 | * if (!isset($result[$value])) 70 | * $result[$value] = []; 71 | * 72 | * $result[$value][] = $key; 73 | * 74 | * return $result; 75 | * }, []) 76 | * ``` 77 | * 78 | * **Result** 79 | * 80 | * ``` 81 | * [ 82 | * '1' => ['a', 'c'], 83 | * '2' => ['b'] 84 | * ] 85 | * ``` 86 | * 87 | * @since 0.2.0 added support for iterables 88 | * 89 | * @param iterable|\stdClass $collection The collection to iterate over. 90 | * @param \Closure $iteratee The function invoked per iteration. 91 | * @param array|\stdClass|mixed $accumulator The initial value. 92 | * 93 | * @return array|\stdClass|mixed Returns the accumulated value. 94 | */ 95 | function reduce($collection, \Closure $iteratee, $accumulator = null) 96 | { 97 | if ($accumulator === null) { 98 | $accumulator = \__::first($collection); 99 | } 100 | \__::doForEach( 101 | $collection, 102 | function ($value, $key, $collection) use (&$accumulator, $iteratee) { 103 | $accumulator = $iteratee($accumulator, $value, $key, $collection); 104 | } 105 | ); 106 | return $accumulator; 107 | } 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Guidelines 4 | 5 | :octocat: It is recommended to have an issue open for any work you take on and intend to submit as a pull request - it helps core members review your concept and direction early and is a good way to discuss what you're planning to do. If there is no issue tied to the Pull Request, please include a detailed description in the PR. 6 | 7 | :100: We are aiming for 100% code coverage. Please keep this in mind when you're submitting new code. 8 | 9 | :ok_hand: We use [PSR standard](http://www.php-fig.org/psr/) to keep our code sanitized and easy to follow. 10 | 11 | ## Development Environment 12 | 13 | The library itself requires PHP >= 5.5 but for development purposes, we require PHP >= 8.1. The library itself only needs `ext-mbstring` to handle special characters but for development, we also need `ext-json` available. Use [Composer v2](https://getcomposer.org/download/) for handling dependencies. 14 | 15 | For generating the documentation website, you'll also need Ruby with [`bundle`](https://bundler.io/). 16 | 17 | ## Development setup 18 | 19 | 1. Fork our repository 20 | 2. Clone your forked repository `git clone git@github.com:/bottomline.git` 21 | 3. Install development dependencies `composer install` 22 | 4. Run tests `composer run test` 23 | 24 | Also useful: 25 | 26 | * Run benchmarks: `composer run bench` 27 | * Format the code: `composer run cs-fix` 28 | * Generate the doc: `composer run doc` 29 | * Generate the doc website: `composer run site` 30 | 31 | ## Development checklist 32 | 33 | - Add or update phpDocs for the new function with a **Usage** and **Result** section ; 34 | - Always add tests for the code that you write, including edge cases ; 35 | - Place the new functions where they belong (collections, arrays, utilities, etc.) ; 36 | - Add `README.md` doc entry for the functions ; 37 | - Execute `php composer.phar run doc` script to automatically build an updated `load.php` ; 38 | - Update the benchmark `bench.php`, this helps us to validate the performance of the library ; 39 | - Update `CHANGELOG.md` with your changes. 40 | 41 | ## How to open a PR 42 | 43 | - Create a branch in your forked repository and push your code into it ; 44 | - Create a PR in [bottomline](https://github.com/maciejczyzewski/bottomline) that points to your forked branch ; 45 | - Add description of the PR (issue links, etc.). 46 | 47 | ### PR Title Recommendations 48 | 49 | - Squash commits into 1 commit before the PR is merged into master, this help reduces git tree and makes it easier to revert to a certain state. (can be multiple commits, but they have to be meaningful commits) ; 50 | - Open PR title starting with one of the following words: 51 | + `Fix` - for a bug fix ; 52 | + `New` - implemented a new feature ; 53 | + `Update` - for a backwards-compatible enhancement ; 54 | + `Breaking` - for a backwards-incompatible enhancement or feature ; 55 | + `Docs` - changes to documentation only ; 56 | - Use `LF` line endings. 57 | 58 | ## Release Checklist 59 | 60 | - [ ] Update `CHANGELOG.md` with the new version and date of release 61 | - [ ] Update the `bottomline v0.x.x` comment located in `src/__/__.php` 62 | - [ ] Ensure that any new functions added during this release have an `@since` tag in their phpDocs 63 | -------------------------------------------------------------------------------- /tests/__/Collections/FindIndexTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(2, __::findIndex($data, "explain")); 14 | $this->assertEquals(-1, __::findIndex($data, "nonexistent")); 15 | } 16 | 17 | public function testWithAssociativeArray() 18 | { 19 | $data = [ 20 | "table" => "trick", 21 | "pen" => "defend", 22 | "motherly" => "wide", 23 | "may" => "needle", 24 | "sweat" => "cake", 25 | "sword" => "defend", 26 | ]; 27 | 28 | $this->assertEquals("pen", __::findIndex($data, "defend")); 29 | $this->assertEquals(-1, __::findIndex($data, "nonexistent")); 30 | } 31 | 32 | public function testWithAssociativeArrayMinusOne() 33 | { 34 | $data = [ 35 | -1 => "minusOne", 36 | "-1" => "minusOneStr", 37 | "table" => "trick", 38 | "pen" => "defend", 39 | "motherly" => "wide", 40 | "may" => "needle", 41 | "sweat" => "cake", 42 | "sword" => "defend", 43 | ]; 44 | 45 | $this->assertEquals("pen", __::findIndex($data, "defend")); 46 | $this->assertEquals(-1, __::findIndex($data, "nonexistent")); 47 | $this->assertEquals(-1, __::findIndex($data, "minusOne")); 48 | $this->assertEquals("-1", __::findIndex($data, "minusOneStr")); 49 | $this->assertEquals(null, __::findIndex($data, "minusOne", null)); 50 | } 51 | 52 | public function testWithAssociativeArrayNull() 53 | { 54 | $data = [ 55 | null => "nullValue", 56 | "table" => "trick", 57 | "pen" => "defend", 58 | "motherly" => "wide", 59 | "may" => "needle", 60 | "sweat" => "cake", 61 | "sword" => "defend", 62 | ]; 63 | 64 | $this->assertEquals("pen", __::findIndex($data, "defend")); 65 | $this->assertEquals(-1, __::findIndex($data, "nonexistent")); 66 | $this->assertEquals(null, __::findIndex($data, "nullValue")); 67 | $this->assertEquals(null, __::findIndex($data, "nullValue", null)); 68 | $this->assertEquals(null, __::findIndex($data, "nullValueNop", null)); 69 | } 70 | 71 | public function testWithCallback() 72 | { 73 | $data = [ 74 | "table" => (object)["name" => "trick"], 75 | "pen" => (object)["name" => "defend"], 76 | "motherly" => (object)["name" => "wide"], 77 | "may" => (object)["name" => "needle"], 78 | "sweat" => (object)["name" => "cake"], 79 | "sword" => (object)["name" => "defend"], 80 | ]; 81 | 82 | $this->assertEquals("pen", __::findIndex($data, static function ($object) { 83 | return $object->name === "defend"; 84 | })); 85 | $this->assertEquals(-1, __::findIndex($data, static function ($value) { 86 | return $value === "potato"; 87 | })); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/__/Collections/ConcatDeepTest.php: -------------------------------------------------------------------------------- 1 | [ 17 | ['color' => ['favorite' => 'red', 5], 3], 18 | [10, 'color' => ['favorite' => 'green', 'blue']], 19 | ], 20 | 'expected' => ['color' => ['favorite' => ['red', 'green'], 5, 'blue'], 3, 10], 21 | ], 22 | [ 23 | 'sources' => [ 24 | ['a' => 0], 25 | ['a' => 1, 'b' => 2], 26 | ['c' => 3, 'd' => 4], 27 | ], 28 | 'expected' => ['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4], 29 | ], 30 | [ 31 | 'sources' => [ 32 | (object)['color' => (object)['favorite' => 'red', 5]], 33 | (object)[10, 'color' => (object)['favorite' => 'green', 'blue']], 34 | ], 35 | 'expected' => (object)['color' => (object)['favorite' => ['red', 'green'], 5, 'blue'], 10], 36 | ], 37 | [ 38 | 'sources' => [ 39 | (object)['a' => 0], 40 | (object)['a' => 1, 'b' => 2], 41 | (object)['c' => 3, 'd' => 4], 42 | ], 43 | 'expected' => (object)['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4], 44 | ], 45 | [ 46 | 'sources' => [ 47 | new ArrayIterator(['a' => 0]), 48 | new ArrayIterator(['a' => 1, 'b' => 2]), 49 | new ArrayIterator(['c' => 3, 'd' => 4]), 50 | ], 51 | 'expected' => ['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4], 52 | ], 53 | [ 54 | 'sources' => [ 55 | new MockIteratorAggregate(['color' => (object)['favorite' => 'red', 5]]), 56 | (object)[10, 'color' => (object)['favorite' => 'green', 'blue']], 57 | ], 58 | 'expected' => ['color' => (object)['favorite' => ['red', 'green'], 5, 'blue'], 10], 59 | ], 60 | [ 61 | 'sources' => [ 62 | (object)[], 63 | call_user_func(function () { 64 | yield 'a' => 0; 65 | }), 66 | call_user_func(function () { 67 | yield 'a' => 1; 68 | yield 'b' => 2; 69 | }), 70 | new ArrayIterator(['c' => 3, 'd' => 4]), 71 | ], 72 | 'expected' => (object)['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4], 73 | ], 74 | ]; 75 | } 76 | 77 | /** 78 | * @dataProvider provideConcatDeepCases 79 | * 80 | * @param array $sources 81 | * @param array $expected 82 | */ 83 | public function testConcatDeep($sources, $expected) 84 | { 85 | $actual = call_user_func_array('__::concatDeep', $sources); 86 | 87 | $this->assertEquals($expected, $actual); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/__/collections/set.php: -------------------------------------------------------------------------------- 1 | $key = $value; 12 | return $newObject; 13 | }; 14 | $set_array = function ($array, $key, $value) { 15 | $array[$key] = $value; 16 | return $array; 17 | }; 18 | $set_iterator = function ($array, $key, $value) { 19 | // We ensure we do not modify an iterator original values, 20 | // by making a copy to an array. 21 | $array = iterator_to_array($array); 22 | $array[$key] = $value; 23 | return $array; 24 | }; 25 | $setter = $set_array; 26 | if (\__::isObject($collection) && !($collection instanceof \ArrayAccess)) { 27 | $setter = $set_object; 28 | } 29 | if ($collection instanceof \Iterator) { 30 | $setter = $set_iterator; 31 | } 32 | return call_user_func_array($setter, [$collection, $key, $value]); 33 | } 34 | 35 | /** 36 | * Return a new collection with the item set at index to given value. Index can 37 | * be a path of nested indexes. 38 | * 39 | * - If `$collection` is an object that implements the ArrayAccess interface, 40 | * this function will treat it as an array. 41 | * - If a portion of path doesn't exist, it's created. Arrays are created for 42 | * missing index in an array; objects are created for missing property in an 43 | * object. 44 | * 45 | * This function throws an `\Exception` if the path consists of a non-collection. 46 | * 47 | * **Usage** 48 | * 49 | * ```php 50 | * __::set(['foo' => ['bar' => 'ter']], 'foo.baz.ber', 'fer'); 51 | * ``` 52 | * 53 | * **Result** 54 | * 55 | * ``` 56 | * ['foo' => ['bar' => 'ter', 'baz' => ['ber' => 'fer']]] 57 | * ``` 58 | * 59 | * @param array|iterable|object $collection Collection of values 60 | * @param string $path Key or index. Supports dot notation 61 | * @param mixed $value The value to set at position $key 62 | * 63 | * @throws \Exception if the path consists of a non collection 64 | * 65 | * @return array|object the new collection with the item set 66 | */ 67 | function set($collection, $path, $value = null) 68 | { 69 | if ($path === null) { 70 | return $collection; 71 | } 72 | 73 | $portions = \__::split($path, \__::DOT_NOTATION_DELIMITER, 2); 74 | $key = $portions[0]; 75 | 76 | if (count($portions) === 1) { 77 | return _universal_set($collection, $key, $value); 78 | } 79 | // Here we manage the case where the portion of the path points to nothing, 80 | // or to a value that does not match the type of the source collection 81 | // (e.g. the path portion 'foo.bar' points to an integer value, while we 82 | // want to set a string at 'foo.bar.fun'. We first set an object or array 83 | // - following the current collection type - to 'for.bar' before setting 84 | // 'foo.bar.fun' to the specified value). 85 | if ( 86 | !\__::has($collection, $key) 87 | || (\__::isObject($collection) && !\__::isObject(\__::get($collection, $key))) 88 | || (\__::isArray($collection) && !\__::isArray(\__::get($collection, $key))) 89 | ) { 90 | $collection = _universal_set($collection, $key, (\__::isObject($collection) && !($collection instanceof \ArrayAccess)) ? new \stdClass : []); 91 | } 92 | return _universal_set($collection, $key, set(\__::get($collection, $key), $portions[1], $value)); 93 | } 94 | -------------------------------------------------------------------------------- /tests/__/Arrays/DropWhileTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([3, 4, 5], $out1); 27 | $this->assertEquals([1, 2, 3, 4, 5], $out2); 28 | $this->assertEquals([], $out3); 29 | } 30 | 31 | public function testDropWhileWithPrimitive() 32 | { 33 | $arr = [1, 1, 2, 3, 4, 5]; 34 | 35 | $actual = __::dropWhile($arr, 1); 36 | 37 | $this->assertEquals([2, 3, 4, 5], $actual); 38 | } 39 | 40 | public function testDropWhileWithIterator() 41 | { 42 | $arr = [1, 2, 3, 4, 5]; 43 | $arrItr = new ArrayIterator($arr); 44 | 45 | $f = static function ($item) { 46 | return $item < 3; 47 | }; 48 | $expected = __::dropWhile($arr, $f); 49 | $actual = __::dropWhile($arrItr, $f); 50 | $itrSize = 0; 51 | 52 | foreach ($actual as $i => $item) { 53 | ++$itrSize; 54 | $this->assertEquals($item, $expected[$i]); 55 | } 56 | 57 | $this->assertEquals(count($expected), $itrSize); 58 | } 59 | 60 | public function testDropWhileWithIteratorAggregate() 61 | { 62 | $arr = [1, 2, 3, 4, 5]; 63 | $arrItr = new MockIteratorAggregate($arr); 64 | 65 | $f = static function ($item) { 66 | return $item < 3; 67 | }; 68 | $expected = __::dropWhile($arr, $f); 69 | $actual = __::dropWhile($arrItr, $f); 70 | $itrSize = 0; 71 | 72 | foreach ($actual as $i => $item) { 73 | ++$itrSize; 74 | $this->assertEquals($item, $expected[$i]); 75 | } 76 | 77 | $this->assertEquals(count($expected), $itrSize); 78 | } 79 | 80 | public function testDropWhileWithGenerator() 81 | { 82 | $arr = [1, 2, 3, 4, 5]; 83 | $generator = call_user_func(function () use ($arr) { 84 | foreach ($arr as $item) { 85 | yield $item; 86 | } 87 | }); 88 | 89 | $this->assertInstanceOf(\Generator::class, $generator); 90 | 91 | $f = static function ($item) { 92 | return $item < 3; 93 | }; 94 | $expected = __::dropWhile($arr, $f); 95 | $actual = __::dropWhile($generator, $f); 96 | $itrSize = 0; 97 | 98 | foreach ($actual as $i => $item) { 99 | ++$itrSize; 100 | $this->assertEquals($item, $expected[$i]); 101 | } 102 | 103 | $this->assertEquals(count($expected), $itrSize); 104 | } 105 | 106 | public function testDropWhileWithMatchAfterDroppingEnds() 107 | { 108 | $arr = [1, 1, 2, 1, 1]; 109 | $arrItr = new ArrayIterator($arr); 110 | 111 | $expected = __::dropWhile($arr, 1); 112 | $actual = __::dropWhile($arrItr, 1); 113 | $itrSize = 0; 114 | 115 | foreach ($actual as $i => $item) { 116 | ++$itrSize; 117 | $this->assertEquals($item, $expected[$i]); 118 | } 119 | 120 | $this->assertEquals(count($expected), $itrSize); 121 | } 122 | } 123 | --------------------------------------------------------------------------------