├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── VERSION ├── composer.json ├── phpunit.xml.dist ├── readme.md ├── src ├── Arrayizes.php ├── HigherOrderMessage.php ├── Underscore.php ├── UnderscoreAliases.php ├── UnderscoreArray.php ├── UnderscoreBase.php ├── UnderscoreCollection.php ├── UnderscoreException.php ├── UnderscoreFunction.php └── functions.php └── tests ├── HigherOrderMessageTest.php ├── UnderscoreArrayTest.php ├── UnderscoreBaseTest.php ├── UnderscoreCollectionTest.php ├── UnderscoreFunctionTest.php ├── UnderscoreTest.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | ; 3 | ; Sublime: https://github.com/sindresorhus/editorconfig-sublime 4 | ; Phpstorm: https://plugins.jetbrains.com/plugin/7294-editorconfig 5 | 6 | root = true 7 | 8 | [*] 9 | indent_style = space 10 | indent_size = 4 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [{*.js,*.css,*.scss,*.html}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adhocore 2 | custom: ['https://paypal.me/ji10'] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "22:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | 20 | tests: 21 | name: Tests 22 | env: 23 | extensions: pcov 24 | 25 | strategy: 26 | matrix: 27 | include: 28 | - php: '8.0' 29 | - php: '8.1' 30 | - php: '8.2' 31 | fail-fast: true 32 | 33 | runs-on: ubuntu-20.04 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v2 38 | with: 39 | fetch-depth: 2 40 | 41 | - name: Setup PHP 42 | uses: shivammathur/setup-php@v2 43 | with: 44 | coverage: "none" 45 | ini-values: date.timezone=Asia/Bangkok,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,zend.assertions=1 46 | php-version: "${{ matrix.php }}" 47 | extensions: "${{ env.extensions }}" 48 | tools: flex 49 | 50 | - name: Before run 51 | run: | 52 | echo COLUMNS=120 >> $GITHUB_ENV 53 | for P in src tests; do find $P -type f -name '*.php' -exec php -l {} \;; done 54 | 55 | - name: Install dependencies 56 | run: composer install --no-progress --ansi -o 57 | 58 | - name: Run tests 59 | run: composer test:cov 60 | 61 | - name: Codecov 62 | run: bash <(curl -s https://codecov.io/bash) 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # standards 2 | /.cache/ 3 | /.env 4 | /.idea/ 5 | /vendor/ 6 | composer.lock 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [v1.0.0](https://github.com/adhocore/php-underscore/releases/tag/v1.0.0) (2022-10-02) 2 | 3 | ### Features 4 | - Modernize code for php8+ (Jitendra Adhikari) [_7841d8f_](https://github.com/adhocore/php-underscore/commit/7841d8f) 5 | 6 | ### Miscellaneous 7 | - **Composer**: Req php>=8.0, bump phpunit, add test scripts (Jitendra Adhikari) [_35850f3_](https://github.com/adhocore/php-underscore/commit/35850f3) 8 | - **Travis**: Retire it :( (Jitendra Adhikari) [_a994d44_](https://github.com/adhocore/php-underscore/commit/a994d44) 9 | 10 | ### Builds 11 | - **Workflow**: Add github action build (Jitendra Adhikari) [_c2fb353_](https://github.com/adhocore/php-underscore/commit/c2fb353) 12 | - **Travis**: Add php 7.3, 7.4 (Jitendra Adhikari) [_1abfbf3_](https://github.com/adhocore/php-underscore/commit/1abfbf3) 13 | 14 | 15 | ## [v0.1.0] 2018-08-19 06:08:30 UTC 16 | 17 | - [1c3223c](https://github.com/adhocore/php-underscore/commit/1c3223c) build: temp disable php5.6 (Jitendra Adhikari) 18 | - [39c9550](https://github.com/adhocore/php-underscore/commit/39c9550) refactor (Jitendra Adhikari) 19 | - [1e45992](https://github.com/adhocore/php-underscore/commit/1e45992) test: fix test, we still use php5 (Jitendra Adhikari) 20 | - [94c940a](https://github.com/adhocore/php-underscore/commit/94c940a) docs: add Arrayizes (Jitendra Adhikari) 21 | - [067e8d5](https://github.com/adhocore/php-underscore/commit/067e8d5) docs: add toc, higher order msg (Jitendra Adhikari) 22 | - [43522a6](https://github.com/adhocore/php-underscore/commit/43522a6) test(hom): test call/get (Jitendra Adhikari) 23 | - [a8b34b7](https://github.com/adhocore/php-underscore/commit/a8b34b7) test(hom): add stub, test call/get (Jitendra Adhikari) 24 | - [e455c99](https://github.com/adhocore/php-underscore/commit/e455c99) fix(hom): missing return (Jitendra Adhikari) 25 | - [6ba1916](https://github.com/adhocore/php-underscore/commit/6ba1916) feat(hom): support higher order messaging (Jitendra Adhikari) 26 | - [0b142f4](https://github.com/adhocore/php-underscore/commit/0b142f4) feat(base): support higer order messaging [wip] (Jitendra Adhikari) 27 | - [57f51eb](https://github.com/adhocore/php-underscore/commit/57f51eb) Test enhancement (peter279k) 28 | - [3f9854c](https://github.com/adhocore/php-underscore/commit/3f9854c) docs: fix badge (Jitendra Adhikari) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jitendra Adhikari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v1.0.0 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adhocore/underscore", 3 | "description": "PHP underscore inspired &/or cloned from _.js", 4 | "type": "library", 5 | "keywords": ["php", "underscore", "collection"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Jitendra Adhikari", 10 | "email": "jiten.adhikary@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Ahc\\Underscore\\": "src/" 16 | }, 17 | "files": ["src/functions.php"] 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "Ahc\\Underscore\\Test\\": "tests/" 22 | } 23 | }, 24 | "require": { 25 | "php": ">=8.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.5" 29 | }, 30 | "config": { 31 | "optimize-autoloader": true, 32 | "preferred-install": { 33 | "*": "dist" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/phpunit", 38 | "test:cov": "phpunit --coverage-text --coverage-clover coverage.xml" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## adhocore/underscore 2 | 3 | PHP underscore inspired &/or cloned from awesome `_.js`. A set of utilities and data manipulation helpers providing convenience functionalites to deal with array, list, hash, functions and so on in a neat elegant and OOP way. Guaranteed to save you tons of boiler plate codes when churning complex data collection. 4 | 5 | [![Latest Version](https://img.shields.io/github/release/adhocore/php-underscore.svg?style=flat-square)](https://github.com/adhocore/php-underscore/releases) 6 | [![Travis Build](https://img.shields.io/travis/adhocore/php-underscore/master.svg?style=flat-square)](https://travis-ci.org/adhocore/php-underscore?branch=master) 7 | [![Scrutinizer CI](https://img.shields.io/scrutinizer/g/adhocore/php-underscore.svg?style=flat-square)](https://scrutinizer-ci.com/g/adhocore/php-underscore/?branch=master) 8 | [![Codecov branch](https://img.shields.io/codecov/c/github/adhocore/php-underscore/master.svg?style=flat-square)](https://codecov.io/gh/adhocore/php-underscore) 9 | [![StyleCI](https://styleci.io/repos/108437038/shield)](https://styleci.io/repos/108437038) 10 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 11 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Functional+programming+paradigm+in+PHP+to+manipulate+arrays+like+pro&url=https://github.com/adhocore/php-underscore&hashtags=php,functional,array,collection) 12 | [![Support](https://img.shields.io/static/v1?label=Support&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/adhocore) 13 | 16 | 17 | 18 | - Zero dependency (no vendor bloat). 19 | 20 | ## Installation 21 | 22 | Requires PHP5.6 or later. 23 | 24 | ```sh 25 | composer require adhocore/underscore 26 | ``` 27 | 28 | ## Usage and API 29 | 30 | Although all of them are available with helper function `underscore($data)` or `new Ahc\Underscore($data)`, 31 | the methods are grouped and organized in different heriarchy and classes according as their scope. 32 | This keeps it maintainable and saves from having a God class. 33 | 34 | #### Contents 35 | 36 | - [Underscore](#underscore) 37 | - [UnderscoreFunction](#underscorefunction) 38 | - [UnderscoreArray](#underscorearray) 39 | - [UnderscoreCollection](#underscorecollection) 40 | - [UnderscoreBase](#underscorebase) 41 | - [HigherOrderMessage](#higherordermessage) 42 | - [ArrayAccess](#arrayaccess) 43 | - [Arrayizes](#arrayizes) 44 | 45 | 46 | --- 47 | ### Underscore 48 | 49 |

constant(mixed $value): callable

50 | 51 | Generates a function that always returns a constant value. 52 | 53 | ```php 54 | $fn = underscore()->constant([1, 2]); 55 | 56 | $fn(); // [1, 2] 57 | ``` 58 | 59 |

noop(): void

60 | 61 | No operation! 62 | ```php 63 | underscore()->noop(); // void/null 64 | ``` 65 | 66 |

random(int $min, int $max): int

67 | 68 | Return a random integer between min and max (inclusive). 69 | 70 | ```php 71 | $rand = underscore()->random(1, 10); 72 | ``` 73 | 74 |

times(int $n, callable $fn): self

75 | 76 | Run callable n times and create new collection. 77 | 78 | ```php 79 | $fn = function ($i) { return $i * 2; }; 80 | 81 | underscore()->times(5, $fn)->get(); 82 | // [0, 2, 4, 6, 8] 83 | ``` 84 | 85 |

uniqueId(string $prefix): string

86 | 87 | Generate unique ID (unique for current go/session). 88 | 89 | ```php 90 | $u = underscore()->uniqueId(); // '1' 91 | $u1 = underscore()->uniqueId(); // '2' 92 | $u3 = underscore()->uniqueId('id:'); // 'id:3' 93 | ``` 94 | 95 | 96 | --- 97 | ### UnderscoreFunction 98 | 99 |

compose(callable $fn1, callable $fn2, ...callable|null $fn3): mixed

100 | 101 | Returns a function that is the composition of a list of functions, 102 | each consuming the return value of the function that follows. 103 | 104 | ```php 105 | $c = underscore()->compose('strlen', 'strtolower', 'strtoupper'); 106 | 107 | $c('aBc.xYz'); // ABC.XYZ => abc.xyz => 7 108 | ``` 109 | 110 |

delay(callable $fn, int $wait): mixed

111 | 112 | Cache the result of callback for given arguments and reuse that in subsequent call. 113 | 114 | ```php 115 | $cb = underscore()->delay(function () { echo 'OK'; }, 100); 116 | 117 | // waits 100ms 118 | $cb(); // 'OK' 119 | ``` 120 | 121 |

memoize(callable $fn): mixed

122 | 123 | Returns a callable which when invoked caches the result for given arguments 124 | and reuses that result in subsequent calls. 125 | 126 | ```php 127 | $sum = underscore()->memoize(function ($a, $b) { return $a + $b; }); 128 | 129 | $sum(4, 5); // 9 130 | 131 | // Uses memo: 132 | $sum(4, 5); // 9 133 | ``` 134 | 135 |

throttle(callable $fn, int $wait): mixed

136 | 137 | Returns a callable that wraps given callable which can be only invoked 138 | at most once per given $wait threshold. 139 | 140 | ```php 141 | $fn = underscore()->throttle($callback, 100); 142 | 143 | while (...) { 144 | $fn(); // it will be constantly called but not executed more than one in 100ms 145 | 146 | if (...) break; 147 | } 148 | ``` 149 | 150 | 151 | --- 152 | ### UnderscoreArray 153 | 154 |

compact(): self

155 | 156 | Get only the truthy items. 157 | 158 | ```php 159 | underscore($array)->compact()->get(); 160 | // [1 => 'a', 4 => 2, 5 => [1] 161 | ``` 162 | 163 |

difference(array|mixed $data): self

164 | 165 | Get the items whose value is not in given data. 166 | 167 | ```php 168 | underscore([1, 2, 1, 'a' => 3, 'b' => [4]])->difference([1, [4]])->get(); 169 | // [1 => 2, 'a' => 3] 170 | ``` 171 | 172 |

findIndex(callable $fn): mixed|null

173 | 174 | Find the first index that passes given truth test. 175 | 176 | ```php 177 | $u = underscore([[1, 2], 'a' => 3, 'x' => 4, 'y' => 2, 'b' => 'B']); 178 | 179 | $isEven = function ($i) { return is_numeric($i) && $i % 2 === 0; }; 180 | 181 | $u->findIndex(); // 0 182 | $u->findIndex($isEven); // 'x' 183 | ``` 184 | 185 |

findLastIndex(callable $fn): mixed|null

186 | 187 | Find the last index that passes given truth test. 188 | 189 | ```php 190 | $u = underscore([[1, 2], 'a' => 3, 'x' => 4, 'y' => 2, 'b' => 'B']); 191 | 192 | $isEven = function ($i) { return is_numeric($i) && $i % 2 === 0; }; 193 | 194 | $u->findLastIndex(); // 'b' 195 | $u->findLastIndex($isEven); // 'y' 196 | ``` 197 | 198 |

first(int $n): array|mixed

199 | 200 | Get the first n items. 201 | 202 | ```php 203 | underscore([1, 2, 3])->first(); // 1 204 | underscore([1, 2, 3])->first(2); // [1, 2] 205 | ``` 206 | 207 |

flatten(): self

208 | 209 | Gets the flattened version of multidimensional items. 210 | 211 | ```php 212 | $u = underscore([0, 'a', '', [[1, [2]]], 'b', [[[3]], 4, 'c', underscore([5, 'd'])]]); 213 | 214 | $u->flatten()->get(); // [0, 'a', '', 1, 2, 'b', 3, 4, 'c', 5, 'd'] 215 | ``` 216 | 217 |

indexOf(mixed $value): string|int|null

218 | 219 | Find the first index of given value if available null otherwise. 220 | 221 | ```php 222 | $u = underscore([[1, 2], 'a' => 2, 'x' => 4]); 223 | 224 | $array->indexOf(2); // 'a' 225 | ``` 226 | 227 |

intersection(array|mixed $data): self

228 | 229 | Gets the items whose value is common with given data. 230 | 231 | ```php 232 | $u = underscore([1, 2, 'a' => 3]); 233 | 234 | $u->intersection([2, 'a' => 3, 3])->get(); // [1 => 2, 'a' => 3] 235 | ``` 236 | 237 |

last(int $n): array|mixed

238 | 239 | Get the last n items. 240 | 241 | ```php 242 | underscore([1, 2, 3])->last(); // 3 243 | underscore([1, 2, 3])->last(2); // [2, 3] 244 | ``` 245 | 246 |

lastIndexOf(mixed $value): string|int|null

247 | 248 | Find the last index of given value if available null otherwise. 249 | 250 | ```php 251 | $u = underscore([[1, 2], 'a' => 2, 'x' => 4, 'y' => 2]); 252 | 253 | $array->lastIndexOf(2); // 'y' 254 | ``` 255 | 256 |

object(string|null $className): self

257 | 258 | Hydrate the items into given class or stdClass. 259 | 260 | ```php 261 | underscore(['a', 'b' => 2])->object(); // stdClass(0: 'a', 'b': 2) 262 | ``` 263 | 264 |

range(int $start, int $stop, int $step): self

265 | 266 | Creates a new range from start to stop with given step. 267 | 268 | ```php 269 | underscore()->range(4, 9)->get(); // [4, 5, 6, 7, 8, 9] 270 | ``` 271 | 272 |

sortedIndex(mixed $object, callable|string $fn): string|int|null

273 | 274 | Gets the smallest index at which an object should be inserted so as to maintain order. 275 | 276 | ```php 277 | underscore([1, 3, 5, 8, 11])->sortedIndex(9, null); // 4 278 | ``` 279 | 280 |

union(array|mixed $data): self

281 | 282 | Get the union/merger of items with given data. 283 | 284 | ```php 285 | $u = underscore([1, 2, 'a' => 3]); 286 | 287 | $u->union([3, 'a' => 4, 'b' => [5]])->get(); // [1, 2, 'a' => 4, 3, 'b' => [5]] 288 | ``` 289 | 290 |

unique(callable|string $fn): self

291 | 292 | Gets the unique items using the id resulted from callback. 293 | 294 | ```php 295 | $u = underscore([1, 2, 'a' => 3]); 296 | 297 | $u->union([3, 'a' => 4, 'b' => [5]])->get(); 298 | // [1, 2, 'a' => 4, 3, 'b' => [5]] 299 | ``` 300 | 301 |

zip(array|mixed $data): self

302 | 303 | Group the values from data and items having same indexes together. 304 | 305 | ```php 306 | $u = underscore([1, 2, 'a' => 3, 'b' => 'B']); 307 | 308 | $u->zip([2, 4, 'a' => 5])->get(); 309 | // [[1, 2], [2, 4], 'a' => [3, 5], 'b' => ['B', null]] 310 | ``` 311 | 312 | 313 | --- 314 | ### UnderscoreCollection 315 | 316 |

contains(mixed $item): bool

317 | 318 | Check if the collection contains given item. 319 | 320 | ```php 321 | $u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]); 322 | 323 | $u->contains(1); // true 324 | $u->contains('x'); // false 325 | ``` 326 | 327 |

countBy(callable|string $fn): self

328 | 329 | Count items in each group indexed by the result of callback. 330 | 331 | ```php 332 | $u = underscore([ 333 | ['a' => 0, 'b' => 1, 'c' => 1], 334 | ['a' => true, 'b' => false, 'c' => 'c'], 335 | ['a' => 2, 'b' => 1, 'c' => 2], 336 | ['a' => 1, 'b' => null, 'c' => 0], 337 | ]); 338 | 339 | // by key 'a' 340 | $u->countBy('a')->get(); 341 | // [0 => 1, 1 => 2, 2 => 1] 342 | ``` 343 | 344 |

each(callable $fn): self

345 | 346 | Apply given callback to each of the items in collection. 347 | 348 | ```php 349 | $answers = []; 350 | underscore([1, 2, 3])->each(function ($num) use (&$answers) { 351 | $answers[] = $num * 5; 352 | }); 353 | 354 | $answers; // [5, 10, 15] 355 | ``` 356 | 357 |

every(callable $fn): bool

358 | 359 | Tests if all the items pass given truth test. 360 | 361 | ```php 362 | $gt0 = underscore([1, 2, 3, 4])->every(function ($num) { return $num > 0; }); 363 | 364 | $gt0; // true 365 | ``` 366 | 367 |

filter(callable|string|null $fn): self

368 | 369 | Find and return all the items that passes given truth test. 370 | 371 | ```php 372 | $gt2 = underscore([1, 2, 4, 0, 3])->filter(function ($num) { return $num > 2; }); 373 | 374 | $gt2->values(); // [4, 3] 375 | ``` 376 | 377 |

find(callable $fn, bool $useValue): mixed|null

378 | 379 | Find the first item (or index) that passes given truth test. 380 | 381 | ```php 382 | $num = underscore([1, 2, 4, 3])->find(function ($num) { return $num > 2; }); 383 | 384 | $num; // 4 385 | 386 | $idx = underscore([1, 2, 4, 3])->find(function ($num) { return $num > 2; }, false); 387 | 388 | $idx; // 2 389 | ``` 390 | 391 |

findWhere(array $props): mixed

392 | 393 | Get the first item that contains all the given props (matching both index and value). 394 | 395 | ```php 396 | $u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3]]); 397 | 398 | $u->findWhere(['b' => 3]); // ['a' => 1, 'b' => 3] 399 | ``` 400 | 401 |

groupBy(callable|string $fn): self

402 | 403 | Group items by using the result of callback as index. The items in group will have original index intact. 404 | 405 | ```php 406 | $u = underscore([ 407 | ['a' => 0, 'b' => 1, 'c' => 1], 408 | ['a' => true, 'b' => false, 'c' => 'c'], 409 | ['a' => 2, 'b' => 1, 'c' => 2], 410 | ['a' => 1, 'b' => null, 'c' => 0], 411 | ]); 412 | 413 | // by key 'a' 414 | $u->groupBy('a')->get(); 415 | // [ 416 | // 0 => [0 => ['a' => 0, 'b' => 1, 'c' => 1]], 417 | // 1 => [1 => ['a' => true, 'b' => false, 'c' => 'c'], 3 => ['a' => 1, 'b' => null, 'c' => 0]], 418 | // 2 => [2 => ['a' => 2, 'b' => 1, 'c' => 2]], 419 | // ] 420 | ``` 421 | 422 |

indexBy(callable|string $fn): self

423 | 424 | Reindex items by using the result of callback as new index. 425 | 426 | ```php 427 | $u = underscore([ 428 | ['a' => 0, 'b' => 1, 'c' => 1], 429 | ['a' => true, 'b' => false, 'c' => 'c'], 430 | ['a' => 2, 'b' => 1, 'c' => 2], 431 | ['a' => 1, 'b' => null, 'c' => 0], 432 | ]); 433 | 434 | // by key 'a' 435 | $u->indexBy('a')->get(); 436 | // [ 437 | // 0 => ['a' => 0, 'b' => 1, 'c' => 1], 438 | // 1 => ['a' => 1, 'b' => null, 'c' => 0], 439 | // 2 => ['a' => 2, 'b' => 1, 'c' => 2], 440 | // ] 441 | ``` 442 | 443 |

invoke(callable $fn): mixed

444 | 445 | Invoke a callback using all of the items as arguments. 446 | 447 | ```php 448 | $sum = underscore([1, 2, 4])->invoke(function () { return array_sum(func_get_args()); }); 449 | 450 | $sum; // 7 451 | ``` 452 | 453 |

map(callable $fn): self

454 | 455 | Update the value of each items with the result of given callback. 456 | 457 | ```php 458 | $map = underscore([1, 2, 3])->map(function ($num) { return $num * 2; }); 459 | 460 | $map->get(); // [2, 4, 6] 461 | ``` 462 | 463 |

max(callable|string|null $fn): mixed

464 | 465 | Find the maximum value using given callback or just items. 466 | 467 | ```php 468 | underscore([1, 5, 4])->max(); // 5 469 | $u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]); 470 | 471 | $u->max(function ($i) { return $i['a'] + $i['b']; }); // 5 472 | ``` 473 | 474 |

min(callable|string|null $fn): mixed

475 | 476 | Find the minimum value using given callback or just items. 477 | 478 | ```php 479 | underscore([1, 5, 4])->min(); // 1 480 | $u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]); 481 | 482 | $u->min(function ($i) { return $i['a'] + $i['b']; }); // 1 483 | ``` 484 | 485 |

partition(callable|string $fn): self

486 | 487 | Separate the items into two groups: one passing given truth test and other failing. 488 | 489 | ```php 490 | $u = underscore(range(1, 10)); 491 | 492 | $oddEvn = $u->partition(function ($i) { return $i % 2; }); 493 | 494 | $oddEvn->get(0); // [1, 3, 5, 7, 9] 495 | $oddEvn->get(1); // [2, 4, 6, 8, 10] 496 | ``` 497 | 498 |

pluck(string|int $columnKey, string|int $indexKey): self

499 | 500 | Pluck given property from each of the items. 501 | 502 | ```php 503 | $u = underscore([['name' => 'moe', 'age' => 30], ['name' => 'curly']]); 504 | 505 | $u->pluck('name')->get(); // ['moe', 'curly'] 506 | ``` 507 | 508 |

reduce(callable $fn, mixed $memo): mixed

509 | 510 | Iteratively reduce the array to a single value using a callback function. 511 | 512 | ```php 513 | $sum = underscore([1, 2, 3])->reduce(function ($sum, $num) { 514 | return $num + $sum; 515 | }, 0); 516 | 517 | $sum; // 6 518 | ``` 519 | 520 |

reduceRight(callable $fn, mixed $memo): mixed

521 | 522 | Same as reduce but applies the callback from right most item first. 523 | 524 | ```php 525 | $concat = underscore([1, 2, 3, 4])->reduceRight(function ($concat, $num) { 526 | return $concat . $num; 527 | }, ''); 528 | 529 | echo $concat; // '4321' 530 | ``` 531 | 532 |

reject(callable $fn): self

533 | 534 | Find and return all the items that fails given truth test. 535 | 536 | ```php 537 | $evens = underscore([1, 2, 3, 4, 5, 7, 6])->reject(function ($num) { 538 | return $num % 2 !== 0; 539 | }); 540 | 541 | $evens->values(); // [2, 4, 6] 542 | ``` 543 | 544 |

sample(int $n): self

545 | 546 | Get upto n items in random order. 547 | 548 | ```php 549 | $u = underscore([1, 2, 3, 4]); 550 | 551 | $u->sample(1)->count(); // 1 552 | $u->sample(2)->count(); // 2 553 | ``` 554 | 555 |

shuffle(): self

556 | 557 | Randomize the items keeping the indexes intact. 558 | 559 | ```php 560 | underscore([1, 2, 3, 4])->shuffle()->get(); 561 | ``` 562 | 563 |

some(callable $fn): bool

564 | 565 | Tests if some (at least one) of the items pass given truth test. 566 | 567 | ```php 568 | $some = underscore([1, 2, 0, 4, -1])->some(function ($num) { 569 | return $num > 0; 570 | }); 571 | 572 | $some; // true 573 | ``` 574 | 575 |

sortBy(callable $fn): self

576 | 577 | Sort items by given callback and maintain indexes. 578 | 579 | ```php 580 | $u = underscore(range(1, 15))->shuffle(); // randomize 581 | $u->sortBy(null)->get(); // [1, 2, ... 15] 582 | 583 | $u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]); 584 | $u->sortBy('a')->get(); 585 | // [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]] 586 | 587 | $u->sortBy(function ($i) { return $i['a'] + $i['b']; })->get(); 588 | // [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]], 589 | ``` 590 | 591 |

where(array $props): self

592 | 593 | Filter only the items that contain all the given props (matching both index and value). 594 | 595 | ```php 596 | $u = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3, 'c']]); 597 | 598 | $u->where(['a' => 1, 'b' => 2])->get(); // [['a' => 1, 'b' => 2, 'c']] 599 | ``` 600 | 601 | 602 | --- 603 | ### UnderscoreBase 604 | 605 | #### `_`(array|mixed $data): self 606 | 607 | A static shortcut to constructor. 608 | 609 | ```php 610 | $u = Ahc\Underscore\Underscore::_([1, 3, 7]); 611 | ``` 612 | 613 | #### `__`toString(): string 614 | 615 | Stringify the underscore instance as json string. 616 | 617 | ```php 618 | echo (string) underscore([1, 2, 3]); // [1, 2, 3] 619 | echo (string) underscore(['a', 2, 'c' => 3]); // {0: "a", 1: 2, "c":3} 620 | ``` 621 | 622 |

asArray(mixed $data, bool $cast): array

623 | 624 | Get data as array. 625 | 626 | ```php 627 | underscore()->asArray('one'); // ['one'] 628 | underscore()->asArray([1, 2]); // [1, 2] 629 | underscore()->asArray(underscore(['a', 1, 'c', 3])); // ['a', 1, 'c', 3] 630 | 631 | underscore()->asArray(new class { 632 | public function toArray() 633 | { 634 | return ['a', 'b', 'c']; 635 | } 636 | }); // ['a', 'b', 'c'] 637 | 638 | underscore()->asArray(new class implements \JsonSerializable { 639 | public function jsonSerialize() 640 | { 641 | return ['a' => 1, 'b' => 2, 'c' => 3]; 642 | } 643 | }); // ['a' => 1, 'b' => 2, 'c' => 3] 644 | ``` 645 | 646 |

clon(): self

647 | 648 | Creates a shallow copy of itself. 649 | 650 | ```php 651 | $u = underscore(['will', 'be', 'cloned']); 652 | 653 | $u->clon() == $u; // true 654 | $u->clon() === $u; // false 655 | ``` 656 | 657 |

count(): int

658 | 659 | Gets the count of items. 660 | 661 | ```php 662 | underscore([1, 2, [3, 4]])->count(); // 3 663 | underscore()->count(); // 0 664 | ``` 665 | 666 |

flat(array $array): array

667 | 668 | Flatten a multi dimension array to 1 dimension. 669 | 670 | ```php 671 | underscore()->flat([1, 2, [3, 4, [5, 6]]]); // [1, 2, 3, 4, 5, 6] 672 | ``` 673 | 674 |

get(string|int|null $index): mixed

675 | 676 | Get the underlying array data by index. 677 | 678 | ```php 679 | $u = underscore([1, 2, 3]); 680 | 681 | $u->get(); // [1, 2, 3] 682 | $u->get(1); // 2 683 | $u->get(3); // 3 684 | 685 | ``` 686 | 687 |

getData(): array

688 | 689 | Get data. 690 | 691 | ```php 692 | // same as `get()` without args: 693 | underscore([1, 2, 3])->getData(); // [1, 2, 3] 694 | ``` 695 | 696 |

getIterator(): \ArrayIterator

697 | 698 | Gets the iterator for looping. 699 | 700 | ```php 701 | $it = underscore([1, 2, 3])->getIterator(); 702 | 703 | while ($it->valid()) { 704 | echo $it->current(); 705 | } 706 | ``` 707 | 708 |

invert(): self

709 | 710 | Swap index and value of all the items. The values should be stringifiable. 711 | 712 | ```php 713 | $u = underscore(['a' => 1, 'b' => 2, 'c' => 3]); 714 | 715 | $u->invert()->get(); // [1 => 'a', 2 => 'b', 3 => 'c'] 716 | ``` 717 | 718 |

jsonSerialize(): array

719 | 720 | Gets the data for json serialization. 721 | 722 | ```php 723 | $u = underscore(['a' => 1, 'b' => 2, 'c' => 3]); 724 | 725 | json_encode($u); // {"a":1,"b":2,"c":3} 726 | ``` 727 | 728 |

keys(): self

729 | 730 | Get all the keys. 731 | 732 | ```php 733 | $u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]); 734 | 735 | $u->keys()->get(); // ['a', 'b', 'c', 0] 736 | ``` 737 | 738 |

mixin(string $name, \Closure $fn): self

739 | 740 | Adds a custom handler/method to instance. The handler is bound to this instance. 741 | 742 | ```php 743 | Ahc\Underscore\Underscore::mixin('square', function () { 744 | return $this->map(function ($v) { return $v * $v; }); 745 | }); 746 | 747 | underscore([1, 2, 3])->square()->get(); // [1, 4, 9] 748 | ``` 749 | 750 |

now(): float

751 | 752 | The current time in millisec. 753 | 754 | ```php 755 | underscore()->now(); // 1529996371081 756 | ``` 757 | 758 |

omit(array|...string|...int $index): self

759 | 760 | Omit the items having one of the blacklisted indexes. 761 | 762 | ```php 763 | $u = underscore(['a' => 3, 7, 'b' => 'B', 1 => ['c', 5]]); 764 | 765 | $u->omit('a', 0)->get(); // ['b' => 'B', 1 => ['c', 5]] 766 | ``` 767 | 768 |

pairs(): self

769 | 770 | Pair all items to use an array of index and value. 771 | 772 | ```php 773 | $u = ['a' => 3, 7, 'b' => 'B']; 774 | 775 | $u->pair(); // ['a' => ['a', 3], 0 => [0, 7], 'b' => ['b', 'B'] 776 | ``` 777 | 778 |

pick(array|...string|...int $index): self

779 | 780 | Pick only the items having one of the whitelisted indexes. 781 | 782 | ```php 783 | $u = underscore(['a' => 3, 7, 'b' => 'B', 1 => ['c', 5]]); 784 | 785 | $u->pick(0, 1)->get(); // [7, 1 => ['c', 5]] 786 | ``` 787 | 788 |

tap(callable $fn): self

789 | 790 | Invokes callback fn with clone and returns original self. 791 | 792 | ```php 793 | $u = underscore([1, 2]); 794 | 795 | $tap = $u->tap(function ($_) { return $_->values(); }); 796 | 797 | $tap === $u; // true 798 | ``` 799 | 800 |

toArray(): array

801 | 802 | Convert the data items to array. 803 | 804 | ```php 805 | $u = underscore([1, 3, 5, 7]); 806 | 807 | $u->toArray(); // [1, 3, 5, 7] 808 | ``` 809 | 810 |

valueOf(): string

811 | 812 | Get string value (JSON representation) of this instance. 813 | 814 | ```php 815 | underscore(['a', 2, 'c' => 3])->valueOf(); // {0: "a", 1: 2, "c":3} 816 | ``` 817 | 818 |

values(): self

819 | 820 | Get all the values. 821 | 822 | ```php 823 | $u = underscore(['a' => 1, 'b' => 2, 'c' => 3, 5]); 824 | 825 | $u->values()->get(); // [1, 2, 3, 5] 826 | ``` 827 | 828 | 829 | --- 830 | ### UnderscoreAliases 831 | 832 |

collect(callable $fn): self

833 | 834 | Alias of map(). 835 | 836 |

detect(callable $fn, bool $useValue): mixed|null

837 | 838 | Alias of find(). 839 | 840 |

drop(int $n): array|mixed

841 | 842 | Alias of last(). 843 | 844 |

foldl(callable $fn, mixed $memo): mixed

845 | 846 | Alias of reduce(). 847 | 848 |

foldr(callable $fn, mixed $memo): mixed

849 | 850 | Alias of reduceRight(). 851 | 852 |

head(int $n): array|mixed

853 | 854 | Alias of first(). 855 | 856 |

includes(): void

857 | 858 | Alias of contains(). 859 | 860 |

inject(callable $fn, mixed $memo): mixed

861 | 862 | Alias of reduce(). 863 | 864 |

select(callable|string|null $fn): self

865 | 866 | Alias of filter(). 867 | 868 |

size(): int

869 | 870 | Alias of count(). 871 | 872 |

tail(int $n): array|mixed

873 | 874 | Alias of last(). 875 | 876 |

take(int $n): array|mixed

877 | 878 | Alias of first(). 879 | 880 |

uniq(callable|string $fn): self

881 | 882 | Alias of unique(). 883 | 884 |

without(array|mixed $data): self

885 | 886 | Alias of difference(). 887 | 888 | --- 889 | ### HigherOrderMessage 890 | 891 | A syntatic sugar to use elegant shorthand oneliner for complex logic often wrapped in closures. 892 | See example below: 893 | 894 | ```php 895 | // Higher Order Messaging 896 | class HOM 897 | { 898 | protected $n; 899 | public $square; 900 | 901 | public function __construct($n) 902 | { 903 | $this->n = $n; 904 | $this->square = $n * $n; 905 | } 906 | 907 | public function even() 908 | { 909 | return $this->n % 2 === 0; 910 | } 911 | } 912 | 913 | $u = [new HOM(1), new HOM(2), new HOM(3), new HOM(4)]; 914 | 915 | // Filter `even()` items 916 | $evens = $u->filter->even(); // 'even()' method of each items! 917 | 918 | // Map each evens to their squares 919 | $squares = $evens->map->square; // 'square' prop of each items! 920 | // Gives an Underscore instance 921 | 922 | // Get the data 923 | $squares->get(); 924 | // [1 => 4, 3 => 16] 925 | ``` 926 | 927 | Without higher order messaging that would look like: 928 | 929 | ```php 930 | $evens = $u->filter(function ($it) { 931 | return $it->even(); 932 | }); 933 | 934 | $squares = $evens->map(function ($it) { 935 | return $it->square; 936 | }); 937 | ``` 938 | 939 | --- 940 | ### \ArrayAccess 941 | 942 | Underscore instances can be treated as array: 943 | 944 | ```php 945 | $u = underscore([1, 2, 'a' => 3]); 946 | 947 | isset($u['a']); // true 948 | isset($u['b']); // false 949 | 950 | echo $u[1]; // 2 951 | 952 | $u['b'] = 'B'; 953 | isset($u['b']); // true 954 | 955 | unset($u[1]); 956 | ``` 957 | 958 | --- 959 | ### Arrayizes 960 | 961 | You can use this trait to arrayize all complex data. 962 | 963 | ```php 964 | use Ahc\Underscore\Arrayizes; 965 | 966 | class Any 967 | { 968 | use Arrayizes; 969 | 970 | public function name() 971 | { 972 | $this->asArray($data); 973 | } 974 | } 975 | ``` 976 | 977 | --- 978 | #### License 979 | 980 | > [MIT](./LICENSE) | © 2017-2018 | Jitendra Adhikari 981 | -------------------------------------------------------------------------------- /src/Arrayizes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | trait Arrayizes 15 | { 16 | /** 17 | * Get data as array. 18 | * 19 | * @param mixed $data Arbitrary data. 20 | * @param bool $cast Force casting to array! 21 | * 22 | * @return array 23 | */ 24 | public function asArray(mixed $data, bool $cast = true): array 25 | { 26 | if (\is_array($data)) { 27 | return $data; 28 | } 29 | 30 | if ($data instanceof static) { 31 | return $data->get(); 32 | } 33 | 34 | // @codeCoverageIgnoreStart 35 | if ($data instanceof \Traversable) { 36 | return \iterator_to_array($data); 37 | } 38 | // @codeCoverageIgnoreEnd 39 | 40 | if ($data instanceof \JsonSerializable) { 41 | return $data->jsonSerialize(); 42 | } 43 | 44 | if (\is_object($data) && \method_exists($data, 'toArray')) { 45 | return $data->toArray(); 46 | } 47 | 48 | return $cast ? (array) $data : $data; 49 | } 50 | 51 | /** 52 | * Convert the data items to array. 53 | */ 54 | public function toArray(): array 55 | { 56 | return \array_map( 57 | fn ($value) => \is_scalar($value) ? $value : $this->asArray($value, false), 58 | $this->getData() 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/HigherOrderMessage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | /** 15 | * Source: Laravel HigherOrderProxy. 16 | * 17 | * @link https://github.com/laravel/framework/pull/16267 18 | */ 19 | class HigherOrderMessage 20 | { 21 | public function __construct(protected UnderscoreBase $underscore, protected string $method) 22 | { 23 | } 24 | 25 | public function __call(string $method, array $args): mixed 26 | { 27 | return $this->underscore->{$this->method}(static fn ($item) => \call_user_func_array([$item, $method], $args)); 28 | } 29 | 30 | public function __get($prop): mixed 31 | { 32 | return $this->underscore->{$this->method}(static fn ($item) => \array_column([$item], $prop)[0] ?? null); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Underscore.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | final class Underscore extends UnderscoreFunction 15 | { 16 | /** 17 | * Constructor. 18 | * 19 | * @param array|mixed $data 20 | */ 21 | public function __construct(mixed $data = []) 22 | { 23 | parent::__construct($data); 24 | } 25 | 26 | /** 27 | * A static shortcut to constructor. 28 | * 29 | * @param array|mixed $data Array or array like or array convertible. 30 | * 31 | * @return self 32 | */ 33 | public static function _($data = null): self 34 | { 35 | return new static($data); 36 | } 37 | 38 | /** 39 | * Generates a function that always returns a constant value. 40 | */ 41 | public function constant(mixed $value): callable 42 | { 43 | return fn () => $value; 44 | } 45 | 46 | /** 47 | * No operation! 48 | */ 49 | public function noop(): void 50 | { 51 | // ;) 52 | } 53 | 54 | /** 55 | * Run callable n times and create new collection. 56 | */ 57 | public function times(int $n, callable $fn): self 58 | { 59 | $data = []; 60 | 61 | for ($i = 0; $i < $n; $i++) { 62 | $data[$i] = $fn($i); 63 | } 64 | 65 | return new static($data); 66 | } 67 | 68 | /** 69 | * Return a random integer between min and max (inclusive). 70 | */ 71 | public function random(int $min, int $max): int 72 | { 73 | return \mt_rand($min, $max); 74 | } 75 | 76 | /** 77 | * Generate unique ID (unique for current go/session). 78 | */ 79 | public function uniqueId(string $prefix = ''): string 80 | { 81 | static $id = 0; 82 | 83 | return $prefix . (++$id); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/UnderscoreAliases.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | trait UnderscoreAliases 15 | { 16 | /** 17 | * Alias of first(). 18 | */ 19 | public function head(int $n = 1) 20 | { 21 | return $this->first($n); 22 | } 23 | 24 | /** 25 | * Alias of first(). 26 | */ 27 | public function take(int $n = 1) 28 | { 29 | return $this->first($n); 30 | } 31 | 32 | /** 33 | * Alias of last(). 34 | */ 35 | public function tail(int $n = 1) 36 | { 37 | return $this->last($n); 38 | } 39 | 40 | /** 41 | * Alias of last(). 42 | */ 43 | public function drop(int $n = 1) 44 | { 45 | return $this->last($n); 46 | } 47 | 48 | /** 49 | * Alias of unique(). 50 | */ 51 | public function uniq($fn = null): self 52 | { 53 | return $this->unique($fn); 54 | } 55 | 56 | /** 57 | * Alias of difference(). 58 | */ 59 | public function without(mixed $data): self 60 | { 61 | return $this->difference($data); 62 | } 63 | 64 | /** 65 | * Alias of map(). 66 | */ 67 | public function collect(callable $fn): self 68 | { 69 | return $this->map($fn); 70 | } 71 | 72 | /** 73 | * Alias of reduce(). 74 | */ 75 | public function foldl(callable $fn, mixed $memo): mixed 76 | { 77 | return $this->reduce($fn, $memo); 78 | } 79 | 80 | /** 81 | * Alias of reduce(). 82 | */ 83 | public function inject(callable $fn, mixed $memo): mixed 84 | { 85 | return $this->reduce($fn, $memo); 86 | } 87 | 88 | /** 89 | * Alias of reduceRight(). 90 | */ 91 | public function foldr(callable $fn, mixed $memo): mixed 92 | { 93 | return $this->reduceRight($fn, $memo); 94 | } 95 | 96 | /** 97 | * Alias of find(). 98 | */ 99 | public function detect(callable $fn): mixed 100 | { 101 | return $this->find($fn); 102 | } 103 | 104 | /** 105 | * Alias of filter(). 106 | */ 107 | public function select(callable $fn = null): self 108 | { 109 | return $this->filter($fn); 110 | } 111 | 112 | /** 113 | * Alias of every(). 114 | */ 115 | public function all(callable $fn): bool 116 | { 117 | return $this->every($fn); 118 | } 119 | 120 | /** 121 | * Alias of some(). 122 | */ 123 | public function any(callable $fn): bool 124 | { 125 | return $this->some($fn); 126 | } 127 | 128 | /** 129 | * Alias of contains(). 130 | */ 131 | public function includes(mixed $item): bool 132 | { 133 | return $this->contains($item); 134 | } 135 | 136 | /** 137 | * Alias of count(). 138 | */ 139 | public function size(): int 140 | { 141 | return $this->count(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/UnderscoreArray.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | class UnderscoreArray extends UnderscoreCollection 15 | { 16 | /** 17 | * Get the first n items. 18 | * 19 | * @param int $n 20 | * 21 | * @return array|mixed With n = 1 (default), it gives one item, which may not be array. 22 | */ 23 | public function first(int $n = 1): mixed 24 | { 25 | return $this->slice($n, true); 26 | } 27 | 28 | /** 29 | * Get the last n items. 30 | * 31 | * @param int $n 32 | * 33 | * @return array|mixed With n = 1 (default), it gives one item, which may not be array. 34 | */ 35 | public function last(int $n = 1): mixed 36 | { 37 | return $this->slice($n, false); 38 | } 39 | 40 | /** 41 | * Extracts n items from first or last. 42 | * 43 | * @internal 44 | * 45 | * @param int $n 46 | * @param bool $isFirst From first if true, else last. 47 | * 48 | * @return array|mixed With n = 1 (default), it gives one item, which may not be array. 49 | */ 50 | protected function slice(int $n, bool $isFirst = true): mixed 51 | { 52 | if ($n < 2) { 53 | return $isFirst ? \reset($this->data) : \end($this->data); 54 | } 55 | 56 | if ($n >= $c = $this->count()) { 57 | return $this->data; 58 | } 59 | 60 | return \array_slice($this->data, $isFirst ? 0 : $c - $n, $isFirst ? $n : null, true); 61 | } 62 | 63 | /** 64 | * Get only the truthy items. 65 | */ 66 | public function compact(): self 67 | { 68 | return $this->filter(null); 69 | } 70 | 71 | /** 72 | * Gets the flattened version of multidimensional items. 73 | */ 74 | public function flatten(): self 75 | { 76 | return new static($this->flat($this->data)); 77 | } 78 | 79 | /** 80 | * Gets the unique items using the id resulted from callback. 81 | * 82 | * @param callable|string $fn The callback. String is resolved to value of that index. 83 | * 84 | * @return self 85 | */ 86 | public function unique(callable $fn = null): self 87 | { 88 | if (null === $fn) { 89 | return new static(\array_unique($this->data)); 90 | } 91 | 92 | $ids = []; 93 | $fn = $this->valueFn($fn); 94 | 95 | return $this->filter(static function ($value, $index) use ($fn, &$ids) { 96 | return !isset($ids[$id = $fn($value, $index)]) ? $ids[$id] = true : false; 97 | }); 98 | } 99 | 100 | /** 101 | * Get the items whose value is not in given data. 102 | * 103 | * @param array|mixed $data Array or array like or array convertible. 104 | * 105 | * @return self 106 | */ 107 | public function difference(mixed $data): self 108 | { 109 | $data = $this->asArray($data); 110 | 111 | return $this->filter(static fn ($value) => !\in_array($value, $data)); 112 | } 113 | 114 | /** 115 | * Get the union/merger of items with given data. 116 | * 117 | * @param array|mixed $data Array or array like or array convertible. 118 | * 119 | * @return self 120 | */ 121 | public function union(mixed $data): self 122 | { 123 | return new static(\array_merge($this->data, $this->asArray($data))); 124 | } 125 | 126 | /** 127 | * Gets the items whose value is common with given data. 128 | * 129 | * @param array|mixed $data Array or array like or array convertible. 130 | * 131 | * @return self 132 | */ 133 | public function intersection(mixed $data): self 134 | { 135 | $data = $this->asArray($data); 136 | 137 | return $this->filter(static fn ($value) => \in_array($value, $data)); 138 | } 139 | 140 | /** 141 | * Group the values from data and items having same indexes together. 142 | * 143 | * @param array|mixed $data Array or array like or array convertible. 144 | * 145 | * @return self 146 | */ 147 | public function zip(mixed $data): self 148 | { 149 | $data = $this->asArray($data); 150 | 151 | return $this->map(static fn ($value, $idx) => [$value, isset($data[$idx]) ? $data[$idx] : null]); 152 | } 153 | 154 | /** 155 | * Hydrate the items into given class or stdClass. 156 | * 157 | * @param string|null $class FQCN of the class whose constructor accepts two parameters: value and index. 158 | * 159 | * @return self 160 | */ 161 | public function object(string $class = null): self 162 | { 163 | return $this->map( 164 | static fn ($value, $index) => $class ? new $class($value, $index) : (object) \compact('value', 'index') 165 | ); 166 | } 167 | 168 | /** 169 | * Find the first index that passes given truth test. 170 | * 171 | * @param callable $fn The truth test callback. 172 | * 173 | * @return mixed|null 174 | */ 175 | public function findIndex(callable $fn = null): mixed 176 | { 177 | return $this->find($this->valueFn($fn), false); 178 | } 179 | 180 | /** 181 | * Find the last index that passes given truth test. 182 | * 183 | * @param callable $fn The truth test callback. 184 | * 185 | * @return mixed|null 186 | */ 187 | public function findLastIndex(callable $fn = null): mixed 188 | { 189 | return (new static(\array_reverse($this->data, true)))->find($this->valueFn($fn), false); 190 | } 191 | 192 | /** 193 | * Find the first index of given value if available null otherwise. 194 | * 195 | * @param mixed $value The lookup value. 196 | * 197 | * @return string|int|null 198 | */ 199 | public function indexOf(mixed $value): mixed 200 | { 201 | return (false === $index = \array_search($value, $this->data)) ? null : $index; 202 | } 203 | 204 | /** 205 | * Find the last index of given value if available null otherwise. 206 | * 207 | * @param mixed $value The lookup value. 208 | * 209 | * @return string|int|null 210 | */ 211 | public function lastIndexOf(mixed $value): mixed 212 | { 213 | return (false === $index = \array_search($value, \array_reverse($this->data, true))) ? null : $index; 214 | } 215 | 216 | /** 217 | * Gets the smallest index at which an object should be inserted so as to maintain order. 218 | * 219 | * Note that the initial stack must be sorted already. 220 | * 221 | * @param mixed $object The new object which needs to be adjusted in stack. 222 | * @param callable|string $fn The comparator callback. 223 | * 224 | * @return string|int|null 225 | */ 226 | public function sortedIndex(mixed $object, callable $fn = null): mixed 227 | { 228 | $low = 0; 229 | $high = $this->count(); 230 | $data = $this->values(); 231 | $fn = $this->valueFn($fn); 232 | $value = $fn($object); 233 | $keys = $this->keys(); 234 | 235 | while ($low < $high) { 236 | $mid = \intval(($low + $high) / 2); 237 | if ($fn($data[$mid]) < $value) { 238 | $low = $mid + 1; 239 | } else { 240 | $high = $mid; 241 | } 242 | } 243 | 244 | return isset($keys[$low]) ? $keys[$low] : null; 245 | } 246 | 247 | /** 248 | * Creates a new range from start to stop with given step. 249 | * 250 | * @param int $start 251 | * @param int $stop 252 | * @param int $step 253 | * 254 | * @return self 255 | */ 256 | public function range(int $start, int $stop, int $step = 1): self 257 | { 258 | return new static(\range($start, $stop, $step)); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/UnderscoreBase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | class UnderscoreBase implements \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable 15 | { 16 | use Arrayizes; 17 | use UnderscoreAliases; 18 | 19 | const VERSION = '0.0.2'; 20 | 21 | /** @var array The array manipulated by this Underscore instance */ 22 | protected array $data; 23 | 24 | /** @var array Custom userland functionality through named callbacks */ 25 | protected static array $mixins = []; 26 | 27 | /** 28 | * Constructor. Only allow `Ahc\Underscore\Underscore` to be instantiated in userland. 29 | * 30 | * @param array|mixed $data Array or array like or array convertible. 31 | */ 32 | protected function __construct($data = []) 33 | { 34 | $this->data = \is_array($data) ? $data : $this->asArray($data); 35 | } 36 | 37 | /** 38 | * Get the underlying array data. 39 | * 40 | * @param string|int|null $index 41 | * 42 | * @return mixed 43 | */ 44 | public function get(mixed $index = null): mixed 45 | { 46 | if (null === $index) { 47 | return $this->data; 48 | } 49 | 50 | return $this->data[$index] ?? null; 51 | } 52 | 53 | /** 54 | * Get data. 55 | */ 56 | public function getData(): array 57 | { 58 | return $this->data; 59 | } 60 | 61 | /** 62 | * Flatten a multi dimension array to 1 dimension. 63 | */ 64 | public function flat(array $array, &$flat = []): array 65 | { 66 | foreach ($array as $value) { 67 | if ($value instanceof static) { 68 | $value = $value->get(); 69 | } 70 | 71 | if (\is_array($value)) { 72 | $this->flat($value, $flat); 73 | } else { 74 | $flat[] = $value; 75 | } 76 | } 77 | 78 | return $flat; 79 | } 80 | 81 | /** 82 | * Negate a given truth test callable. 83 | */ 84 | protected function negate(callable $fn): callable 85 | { 86 | return static fn () => !$fn(...\func_get_args()); 87 | } 88 | 89 | /** 90 | * Get a value generator callable. 91 | * 92 | * @param callable|string|null $fn 93 | * 94 | * @return callable 95 | */ 96 | protected function valueFn($fn = null): callable 97 | { 98 | if (\is_callable($fn)) { 99 | return $fn; 100 | } 101 | 102 | return static fn ($value) => null === $fn ? $value : \array_column([$value], $fn)[0] ?? null; 103 | } 104 | 105 | /** 106 | * Checks if offset/index exists. 107 | * 108 | * @param string|int $index 109 | * 110 | * @return bool 111 | */ 112 | public function offsetExists(mixed $index): bool 113 | { 114 | return \array_key_exists($index, $this->data); 115 | } 116 | 117 | /** 118 | * Gets the value at given offset/index. 119 | * 120 | * @return mixed 121 | */ 122 | public function offsetGet(mixed $index): mixed 123 | { 124 | return $this->data[$index]; 125 | } 126 | 127 | /** 128 | * Sets a new value at the given offset/index. 129 | * 130 | * @param string|int $index 131 | * @param mixed $value 132 | * 133 | * @return void 134 | */ 135 | public function offsetSet(mixed $index, mixed $value): void 136 | { 137 | $this->data[$index] = $value; 138 | } 139 | 140 | /** 141 | * Unsets/removes the value at given index. 142 | * 143 | * @param string|int $index 144 | * 145 | * @return void 146 | */ 147 | public function offsetUnset(mixed $index): void 148 | { 149 | unset($this->data[$index]); 150 | } 151 | 152 | /** 153 | * Gets the count of items. 154 | */ 155 | public function count(): int 156 | { 157 | return \count($this->data); 158 | } 159 | 160 | /** 161 | * Gets the iterator for looping. 162 | */ 163 | public function getIterator(): \ArrayIterator 164 | { 165 | return new \ArrayIterator($this->data); 166 | } 167 | 168 | /** 169 | * Gets the data for json serialization. 170 | */ 171 | public function jsonSerialize(): array 172 | { 173 | return $this->toArray(); 174 | } 175 | 176 | /** 177 | * Stringify the underscore instance. 178 | */ 179 | public function __toString(): string 180 | { 181 | return \json_encode($this->toArray()); 182 | } 183 | 184 | /** 185 | * The current time in millisec. 186 | * 187 | * @return float 188 | */ 189 | public function now(): float 190 | { 191 | return \microtime(1) * 1000; 192 | } 193 | 194 | /** 195 | * Get all the keys. 196 | */ 197 | public function keys(): self 198 | { 199 | return new static(\array_keys($this->data)); 200 | } 201 | 202 | /** 203 | * Get all the keys. 204 | */ 205 | public function values(): self 206 | { 207 | return new static(\array_values($this->data)); 208 | } 209 | 210 | /** 211 | * Pair all items to use an array of index and value. 212 | */ 213 | public function pairs(): self 214 | { 215 | $pairs = []; 216 | 217 | foreach ($this->data as $index => $value) { 218 | $pairs[$index] = [$index, $value]; 219 | } 220 | 221 | return new static($pairs); 222 | } 223 | 224 | /** 225 | * Swap index and value of all the items. The values should be stringifiable. 226 | */ 227 | public function invert(): self 228 | { 229 | return new static(\array_flip($this->data)); 230 | } 231 | 232 | /** 233 | * Pick only the items having one of the whitelisted indexes. 234 | * 235 | * @param array|...string|...int $index Either whitelisted indexes as array or as variads. 236 | * 237 | * @return self 238 | */ 239 | public function pick($index): self 240 | { 241 | $indices = \array_flip(\is_array($index) ? $index : \func_get_args()); 242 | 243 | return new static(\array_intersect_key($this->data, $indices)); 244 | } 245 | 246 | /** 247 | * Omit the items having one of the blacklisted indexes. 248 | * 249 | * @param array|...string|...int $index Either blacklisted indexes as array or as variads. 250 | * 251 | * @return self 252 | */ 253 | public function omit($index): self 254 | { 255 | $indices = \array_diff( 256 | \array_keys($this->data), 257 | \is_array($index) ? $index : \func_get_args() 258 | ); 259 | 260 | return $this->pick($indices); 261 | } 262 | 263 | /** 264 | * Creates a shallow copy. 265 | */ 266 | public function clon(): self 267 | { 268 | return clone $this; 269 | } 270 | 271 | /** 272 | * Invokes callback fn with clone and returns original self. 273 | */ 274 | public function tap(callable $fn): self 275 | { 276 | $fn($this->clon()); 277 | 278 | return $this; 279 | } 280 | 281 | /** 282 | * Adds a custom handler/method to instance. The handler is bound to this instance. 283 | */ 284 | public static function mixin(string $name, \Closure $fn): void 285 | { 286 | static::$mixins[$name] = $fn; 287 | } 288 | 289 | /** 290 | * Calls the registered mixin by its name. 291 | */ 292 | public function __call(string $method, array $args): self 293 | { 294 | if (isset(static::$mixins[$method])) { 295 | $method = \Closure::bind(static::$mixins[$method], $this); 296 | 297 | return $method($args); 298 | } 299 | 300 | throw new UnderscoreException("The mixin with name '$method' is not defined"); 301 | } 302 | 303 | /** 304 | * Facilitates the use of Higher Order Messaging. 305 | */ 306 | public function __get(string $method): HigherOrderMessage 307 | { 308 | // For now no mixins in HOM :) 309 | if (!\method_exists($this, $method)) { 310 | throw new UnderscoreException("The '$method' is not defined"); 311 | } 312 | 313 | return new HigherOrderMessage($this, $method); 314 | } 315 | 316 | /** 317 | * Get string value (JSON representation) of this instance. 318 | */ 319 | public function valueOf(): string 320 | { 321 | return (string) $this; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/UnderscoreCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | class UnderscoreCollection extends UnderscoreBase 15 | { 16 | /** 17 | * Apply given callback to each of the items in collection. 18 | */ 19 | public function each(callable $fn): self 20 | { 21 | foreach ($this->data as $index => $value) { 22 | $fn($value, $index); 23 | } 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Update the value of each items with the result of given callback. 30 | */ 31 | public function map(callable $fn): self 32 | { 33 | $data = []; 34 | 35 | foreach ($this->data as $index => $value) { 36 | $data[$index] = $fn($value, $index); 37 | } 38 | 39 | return new static($data); 40 | } 41 | 42 | /** 43 | * Iteratively reduce the array to a single value using a callback function. 44 | * 45 | * @param callable $fn The callback. 46 | * @param mixed $memo The initial value carried over to each iteration and returned finally. 47 | * 48 | * @return mixed 49 | */ 50 | public function reduce(callable $fn, $memo) 51 | { 52 | return \array_reduce($this->data, $fn, $memo); 53 | } 54 | 55 | /** 56 | * Same as reduce but applies the callback from right most item first. 57 | * 58 | * @param callable $fn The callback. 59 | * @param mixed $memo The initial value carried over to each iteration and returned finally. 60 | * 61 | * @return mixed 62 | */ 63 | public function reduceRight(callable $fn, mixed $memo): mixed 64 | { 65 | return \array_reduce(\array_reverse($this->data, true), $fn, $memo); 66 | } 67 | 68 | /** 69 | * Find the first item (or index) that passes given truth test. 70 | * 71 | * @param callable $fn The truth test callback. 72 | * @param bool $useValue Whether to return value or the index on match. 73 | * 74 | * @return mixed|null 75 | */ 76 | public function find(callable $fn, bool $useValue = true): mixed 77 | { 78 | foreach ($this->data as $index => $value) { 79 | if ($fn($value, $index)) { 80 | return $useValue ? $value : $index; 81 | } 82 | } 83 | 84 | return null; 85 | } 86 | 87 | /** 88 | * Find and return all the items that passes given truth test. 89 | * 90 | * @param callable|string|null $fn The truth test callback. 91 | * 92 | * @return self 93 | */ 94 | public function filter(callable $fn = null): self 95 | { 96 | if (null === $fn) { 97 | return new static(\array_filter($this->data)); 98 | } 99 | 100 | $data = \array_filter($this->data, $fn, \ARRAY_FILTER_USE_BOTH); 101 | 102 | return new static($data); 103 | } 104 | 105 | /** 106 | * Find and return all the items that fails given truth test. 107 | * 108 | * @param callable $fn The truth test callback. 109 | * 110 | * @return self 111 | */ 112 | public function reject(callable $fn): self 113 | { 114 | $data = \array_filter($this->data, $this->negate($fn), \ARRAY_FILTER_USE_BOTH); 115 | 116 | return new static($data); 117 | } 118 | 119 | /** 120 | * Tests if all the items pass given truth test. 121 | * 122 | * @param callable $fn The truth test callback. 123 | * 124 | * @return bool 125 | */ 126 | public function every(callable $fn): bool 127 | { 128 | return $this->match($fn, true); 129 | } 130 | 131 | /** 132 | * Tests if some (at least one) of the items pass given truth test. 133 | * 134 | * @param callable $fn The truth test callback. 135 | * 136 | * @return bool 137 | */ 138 | public function some(callable $fn): bool 139 | { 140 | return $this->match($fn, false); 141 | } 142 | 143 | /** 144 | * Check if the items match with given truth test. 145 | * 146 | * @internal Used by every() and some(). 147 | * 148 | * @param callable $fn The truth test callback. 149 | * @param bool $all All or one? 150 | * 151 | * @return bool 152 | */ 153 | protected function match(callable $fn, bool $all = true): bool 154 | { 155 | foreach ($this->data as $index => $value) { 156 | if ($all ^ $fn($value, $index)) { 157 | return !$all; 158 | } 159 | } 160 | 161 | return $all; 162 | } 163 | 164 | /** 165 | * Check if the collection contains given item. 166 | */ 167 | public function contains(mixed $item): bool 168 | { 169 | return \in_array($item, $this->data); 170 | } 171 | 172 | /** 173 | * Invoke a callback using all of the items as arguments. 174 | * 175 | * @param callable $fn The callback. 176 | * 177 | * @return mixed Whatever the callback yields. 178 | */ 179 | public function invoke(callable $fn): mixed 180 | { 181 | return $fn(...$this->data); 182 | } 183 | 184 | /** 185 | * Pluck given property from each of the items. 186 | * 187 | * @param string|int $columnKey 188 | * @param string|int $indexKey 189 | * 190 | * @return self 191 | */ 192 | public function pluck(mixed $columnKey, mixed $indexKey = null): self 193 | { 194 | $data = \array_column($this->data, $columnKey, $indexKey); 195 | 196 | return new static($data); 197 | } 198 | 199 | /** 200 | * Filter only the items that contain all the given props (matching both index and value). 201 | */ 202 | public function where(array $props): self 203 | { 204 | return $this->filter($this->matcher($props)); 205 | } 206 | 207 | /** 208 | * Get the first item that contains all the given props (matching both index and value). 209 | */ 210 | public function findWhere(array $props): mixed 211 | { 212 | return $this->find($this->matcher($props)); 213 | } 214 | 215 | /** 216 | * Gives props matcher callback used by where() and findWhere(). 217 | * 218 | * @internal 219 | * 220 | * @param array $props Key value pairs. 221 | * 222 | * @return callable 223 | */ 224 | protected function matcher(array $props): callable 225 | { 226 | return function ($value, $index) use ($props) { 227 | foreach ($props as $prop => $criteria) { 228 | if (\array_column([$value], $prop) != [$criteria]) { 229 | return false; 230 | } 231 | } 232 | 233 | return true; 234 | }; 235 | } 236 | 237 | /** 238 | * Find the maximum value using given callback or just items. 239 | * 240 | * @param callable|string|null $fn The callback. String is resolved to value of that index. 241 | * 242 | * @return mixed 243 | */ 244 | public function max($fn = null): mixed 245 | { 246 | return $this->maxMin($fn, true); 247 | } 248 | 249 | /** 250 | * Find the minimum value using given callback or just items. 251 | * 252 | * @param callable|string|null $fn The callback. String is resolved to value of that index. 253 | * 254 | * @return mixed 255 | */ 256 | public function min($fn = null): mixed 257 | { 258 | return $this->maxMin($fn, false); 259 | } 260 | 261 | /** 262 | * The max/min value retriever used by max() and min(). 263 | * 264 | * @internal 265 | * 266 | * @param callable|string|null $fn The reducer callback. 267 | * @param bool $isMax 268 | * 269 | * @return mixed 270 | */ 271 | protected function maxMin($fn = null, bool $isMax = true): mixed 272 | { 273 | $fn = $this->valueFn($fn); 274 | 275 | return $this->reduce(function ($carry, $value) use ($fn, $isMax) { 276 | $value = $fn($value); 277 | 278 | if (!\is_numeric($value)) { 279 | return $carry; 280 | } 281 | 282 | return null === $carry 283 | ? $value 284 | : ($isMax ? \max($carry, $value) : \min($carry, $value)); 285 | }, null); 286 | } 287 | 288 | /** 289 | * Randomize the items keeping the indexes intact. 290 | * 291 | * @return self 292 | */ 293 | public function shuffle(): self 294 | { 295 | $data = []; 296 | $keys = \array_keys($this->data); 297 | 298 | shuffle($keys); 299 | 300 | foreach ($keys as $index) { 301 | $data[$index] = $this->data[$index]; 302 | } 303 | 304 | return new static($data); 305 | } 306 | 307 | /** 308 | * Get upto n items in random order. 309 | * 310 | * @param int $n Number of items to include. 311 | * 312 | * @return self 313 | */ 314 | public function sample(int $n = 1): self 315 | { 316 | $shuffled = $this->shuffle()->get(); 317 | 318 | return new static(\array_slice($shuffled, 0, $n, true)); 319 | } 320 | 321 | /** 322 | * Sort items by given callback and maintain indexes. 323 | * 324 | * @param callable $fn The callback. Use null to sort based only on values. 325 | * 326 | * @return self 327 | */ 328 | public function sortBy($fn): self 329 | { 330 | $data = $this->map($this->valueFn($fn))->get(); 331 | 332 | \asort($data); // Keep keys. 333 | 334 | foreach ($data as $index => $value) { 335 | $data[$index] = $this->data[$index]; 336 | } 337 | 338 | return new static($data); 339 | } 340 | 341 | /** 342 | * Group items by using the result of callback as index. The items in group will have original index intact. 343 | * 344 | * @param callable|string $fn The callback. String is resolved to value of that index. 345 | * 346 | * @return self 347 | */ 348 | public function groupBy($fn): self 349 | { 350 | return $this->group($fn, true); 351 | } 352 | 353 | /** 354 | * Reindex items by using the result of callback as new index. 355 | * 356 | * @param callable|string $fn The callback. String is resolved to value of that index. 357 | * 358 | * @return self 359 | */ 360 | public function indexBy($fn): self 361 | { 362 | return $this->group($fn, false); 363 | } 364 | 365 | /** 366 | * Count items in each group indexed by the result of callback. 367 | * 368 | * @param callable|string $fn The callback. String is resolved to value of that index. 369 | * 370 | * @return self 371 | */ 372 | public function countBy($fn): self 373 | { 374 | return $this->group($fn, true)->map(fn ($value) => \count($value)); 375 | } 376 | 377 | /** 378 | * Group/index items by using the result of given callback. 379 | * 380 | * @internal 381 | */ 382 | protected function group($fn, bool $isGroup = true): self 383 | { 384 | $data = []; 385 | $fn = $this->valueFn($fn); 386 | 387 | foreach ($this->data as $index => $value) { 388 | $isGroup ? $data[$fn($value, $index)][$index] = $value : $data[$fn($value, $index)] = $value; 389 | } 390 | 391 | return new static($data); 392 | } 393 | 394 | /** 395 | * Separate the items into two groups: one passing given truth test and other failing. 396 | */ 397 | public function partition(callable $fn): self 398 | { 399 | $data = [[/* pass */], [/* fail */]]; 400 | $fn = $this->valueFn($fn); 401 | 402 | $this->each(static function ($value, $index) use ($fn, &$data) { 403 | $data[$fn($value, $index) ? 0 : 1][] = $value; 404 | }); 405 | 406 | return new static($data); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/UnderscoreException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | class UnderscoreException extends \Exception 15 | { 16 | // ;) 17 | } 18 | -------------------------------------------------------------------------------- /src/UnderscoreFunction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore; 13 | 14 | class UnderscoreFunction extends UnderscoreArray 15 | { 16 | /** 17 | * Returns a callable which when invoked caches the result for given arguments 18 | * and reuses that result in subsequent calls. 19 | * 20 | * @param callable $fn The main callback. 21 | * 22 | * @return mixed 23 | */ 24 | public function memoize(callable $fn): mixed 25 | { 26 | static $memo = []; 27 | 28 | return function () use (&$memo, $fn) { 29 | $hash = \md5(\json_encode($args = \func_get_args())); 30 | 31 | if (isset($memo[$hash])) { 32 | return $memo[$hash]; 33 | } 34 | 35 | return $memo[$hash] = $fn(...$args); 36 | }; 37 | } 38 | 39 | /** 40 | * Cache the result of callback for given arguments and reuse that in subsequent call. 41 | * 42 | * @param callable $fn The main callback. 43 | * @param int $wait The time to wait in millisec. 44 | * 45 | * @return mixed 46 | */ 47 | public function delay(callable $fn, $wait) 48 | { 49 | return static function () use ($fn, $wait) { 50 | \usleep(1000 * $wait); 51 | 52 | return $fn(...\func_get_args()); 53 | }; 54 | } 55 | 56 | /** 57 | * Returns a callable that wraps given callable which can be only invoked 58 | * at most once per given $wait threshold. 59 | * 60 | * @param callable $fn The main callback. 61 | * @param int $wait The callback will only be triggered at most once within this period. 62 | * 63 | * @return mixed The return set of callback if runnable else the previous cache. 64 | */ 65 | public function throttle(callable $fn, $wait) 66 | { 67 | static $previous = 0; 68 | static $result = null; 69 | 70 | return function () use ($fn, &$previous, &$result, &$wait) { 71 | $now = $this->now(); 72 | 73 | if (!$previous) { 74 | $previous = $now; 75 | } 76 | 77 | $remaining = $wait - ($now - $previous); 78 | 79 | if ($remaining <= 0 || $remaining > $wait) { 80 | $previous = $now; 81 | $result = $fn(...\func_get_args()); 82 | } 83 | 84 | return $result; 85 | }; 86 | } 87 | 88 | /** 89 | * Returns a function that is the composition of a list of functions, 90 | * each consuming the return value of the function that follows. 91 | * 92 | * Note that last function is executed first. 93 | * 94 | * @param callable $fn1 95 | * @param callable $fn2 96 | * @param ...callable|null $fn3 And so on! 97 | * 98 | * @return mixed Final result value. 99 | */ 100 | public function compose(callable $fn1, callable $fn2 /* , callable $fn3 = null */) 101 | { 102 | $fns = \func_get_args(); 103 | $start = \func_num_args() - 1; 104 | 105 | return static function () use ($fns, $start) { 106 | $i = $start; 107 | $result = $fns[$start](...\func_get_args()); 108 | 109 | while ($i--) { 110 | $result = $fns[$i]($result); 111 | } 112 | 113 | return $result; 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | use Ahc\Underscore\Underscore; 13 | 14 | \class_alias(Underscore::class, 'Ahc\\Underscore'); 15 | 16 | if (!\function_exists('underscore')) { 17 | /** 18 | * Underscore instantiation helper. 19 | * 20 | * @param mixed $data 21 | * 22 | * @return Underscore 23 | */ 24 | function underscore(mixed $data = []): Underscore 25 | { 26 | return new Underscore($data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/HigherOrderMessageTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Test; 13 | 14 | use Ahc\Underscore\HigherOrderMessage; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class HOM 18 | { 19 | public $b = 'B'; 20 | 21 | public function a() 22 | { 23 | return 'A'; 24 | } 25 | } 26 | 27 | /** 28 | * Auto generated by `phint test`. 29 | */ 30 | class HigherOrderMessageTest extends TestCase 31 | { 32 | /** 33 | * @var HigherOrderMessage 34 | */ 35 | protected $hom1; 36 | 37 | /** 38 | * @var HigherOrderMessage 39 | */ 40 | protected $hom2; 41 | 42 | public function setUp(): void 43 | { 44 | parent::setUp(); 45 | 46 | $this->hom1 = new HigherOrderMessage( 47 | underscore([['a' => 1, 'b' => 2]]), 48 | 'map' 49 | ); 50 | 51 | $this->hom2 = new HigherOrderMessage( 52 | underscore([new HOM]), 53 | 'map' 54 | ); 55 | } 56 | 57 | public function test_call() 58 | { 59 | $actual = $this->hom2->a()->get(); 60 | 61 | $this->assertSame(['A'], $actual); 62 | } 63 | 64 | public function test_get() 65 | { 66 | $actual = $this->hom1->b->get(); 67 | 68 | $this->assertSame([2], $actual); 69 | 70 | $actual = $this->hom2->b->get(); 71 | 72 | $this->assertSame(['B'], $actual); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/UnderscoreArrayTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Tests; 13 | 14 | use Ahc\Underscore\Underscore as _; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class UnderscoreArrayTest extends TestCase 18 | { 19 | public function test_first_last() 20 | { 21 | $array = range(rand(5, 10), rand(15, 20)); 22 | 23 | $this->assertSame($array[0], underscore($array)->first(), 'first'); 24 | $this->assertSame(array_reverse($array)[0], underscore($array)->last(), 'last'); 25 | 26 | $array = ['x' => ['first'], 'z' => 'last']; 27 | 28 | $this->assertSame($array['x'], underscore($array)->head(), 'first'); 29 | $this->assertSame($array['z'], underscore($array)->tail(), 'last'); 30 | 31 | $array = range(1, 5); 32 | 33 | $this->assertSame([1, 2, 3], underscore($array)->take(3), 'first 3'); 34 | $this->assertSame([1, 2, 3, 4, 5], underscore($array)->first(6), 'first 6 (n + 1)'); 35 | $this->assertSame([2 => 3, 3 => 4, 4 => 5], underscore($array)->drop(3), 'last 3'); 36 | $this->assertSame([1, 2, 3, 4, 5], underscore($array)->last(6), 'last 6 (n + 1)'); 37 | } 38 | 39 | public function test_compact() 40 | { 41 | $array = [0, 'a', '', [], 2, [1]]; 42 | 43 | $this->assertSame([1 => 'a', 4 => 2, 5 => [1]], underscore($array)->compact()->get(), 'first'); 44 | } 45 | 46 | public function test_flatten() 47 | { 48 | $array = [0, 'a', '', [[1, [2]]], 'b', [[[3]], 4, 'c', new _([5, 'd'])]]; 49 | 50 | $this->assertSame( 51 | [0, 'a', '', 1, 2, 'b', 3, 4, 'c', 5, 'd'], 52 | underscore($array)->flatten()->get(), 53 | 'flatten' 54 | ); 55 | } 56 | 57 | public function test_unique_uniq() 58 | { 59 | $array = [0, 'a', '', 1, '', 0, 2, 'a', 3, 1]; 60 | 61 | $this->assertSame( 62 | [0, 'a', '', 1, 6 => 2, 8 => 3], 63 | underscore($array)->unique()->get(), 64 | 'unique' 65 | ); 66 | 67 | $array = ['a', '', 'a', 1, '', 0, 1, 'b', 3, 2]; 68 | 69 | $this->assertSame( 70 | ['a', '', 3 => 1, 5 => 0, 7 => 'b', 8 => 3, 9 => 2], 71 | underscore($array)->uniq(fn ($i) => $i)->get(), 72 | 'uniq' 73 | ); 74 | } 75 | 76 | public function test_difference_without() 77 | { 78 | $array = [1, 2, 1, 'a' => 3, 'b' => [4]]; 79 | 80 | $this->assertSame( 81 | [1 => 2, 'a' => 3], 82 | underscore($array)->difference([1, [4]])->get(), 83 | 'difference' 84 | ); 85 | 86 | $this->assertSame( 87 | ['a' => 3, 'b' => [4]], 88 | underscore($array)->without([1, 2])->get(), 89 | 'without' 90 | ); 91 | } 92 | 93 | public function test_union() 94 | { 95 | $array = [1, 2, 'a' => 3]; 96 | 97 | $this->assertSame( 98 | [1, 2, 'a' => 4, 3, 'b' => [5]], 99 | underscore($array)->union([3, 'a' => 4, 'b' => [5]])->get(), 100 | 'union' 101 | ); 102 | } 103 | 104 | public function test_intersection() 105 | { 106 | $array = [1, 2, 'a' => 3]; 107 | 108 | $this->assertSame( 109 | [1 => 2, 'a' => 3], 110 | underscore($array)->intersection([2, 'a' => 3, 3])->get(), 111 | 'intersection' 112 | ); 113 | } 114 | 115 | public function test_zip() 116 | { 117 | $array = [1, 2, 'a' => 3, 'b' => 'B']; 118 | 119 | $this->assertSame( 120 | [[1, 2], [2, 4], 'a' => [3, 5], 'b' => ['B', null]], 121 | underscore($array)->zip([2, 4, 'a' => 5])->get(), 122 | 'zip' 123 | ); 124 | } 125 | 126 | public function test_object() 127 | { 128 | $array = [[1, 2], 'a' => 3, 'b' => 'B']; 129 | 130 | foreach (underscore($array)->object() as $index => $value) { 131 | $this->assertTrue(is_object($value)); 132 | $this->assertSame($index, $value->index); 133 | $this->assertSame($array[$index], $value->value); 134 | } 135 | } 136 | 137 | public function test_findIndex_findLastIndex() 138 | { 139 | $array = underscore([[1, 2], 'a' => 3, 'x' => 4, 'y' => 2, 'b' => 'B']); 140 | 141 | $this->assertSame(0, $array->findIndex()); 142 | $this->assertSame('b', $array->findLastIndex()); 143 | 144 | $this->assertSame('x', $array->findIndex(function ($i) { 145 | return is_numeric($i) && $i % 2 === 0; 146 | })); 147 | $this->assertSame('y', $array->findLastIndex(function ($i) { 148 | return is_numeric($i) && $i % 2 === 0; 149 | })); 150 | } 151 | 152 | public function test_indexOf_lastIndexOf() 153 | { 154 | $array = underscore([[1, 2], 'a' => 2, 'x' => 4, 'y' => 2, 'b' => 'B']); 155 | 156 | $this->assertSame('a', $array->indexOf(2)); 157 | $this->assertSame('y', $array->lastIndexOf(2)); 158 | } 159 | 160 | public function test_range() 161 | { 162 | $this->assertSame([4, 5, 6, 7, 8, 9], underscore()->range(4, 9)->get()); 163 | $this->assertSame([10, 12, 14, 16, 18], underscore()->range(10, 18, 2)->get()); 164 | $this->assertSame([20, 19, 18, 17, 16], underscore()->range(20, 16, -1)->get()); 165 | } 166 | 167 | public function test_sortedIndex() 168 | { 169 | $nums = [1, 3, 5, 8, 11]; 170 | $new = 9; 171 | 172 | $newIdx = underscore($nums)->sortedIndex($new, null); 173 | 174 | $this->assertSame(4, $newIdx); 175 | 176 | $data = [ 177 | 'a' => ['x' => 1, 'y' => 2], 178 | 'b' => ['x' => 2, 'y' => 2], 179 | 'c' => ['x' => 3, 'y' => 3], 180 | 'd' => ['x' => 4, 'y' => 3], 181 | 'e' => ['x' => 5, 'y' => 4], 182 | ]; 183 | 184 | $new = ['x' => 3, 'y' => 2]; 185 | $newIdx = underscore($data)->sortedIndex($new, function ($row) { 186 | return $row['x'] + $row['y']; 187 | }); 188 | 189 | $this->assertSame('c', $newIdx); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/UnderscoreBaseTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Tests; 13 | 14 | use Ahc\Underscore\Underscore as _; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class Stub 18 | { 19 | public function toArray() 20 | { 21 | return ['a', 'b', 'c']; 22 | } 23 | } 24 | 25 | class Json implements \JsonSerializable 26 | { 27 | #[\ReturnTypeWillChange] 28 | public function jsonSerialize() 29 | { 30 | return ['a' => 1, 'b' => 2, 'c' => 3]; 31 | } 32 | } 33 | 34 | class Hom 35 | { 36 | public $n; 37 | public $square; 38 | 39 | public function __construct($n) 40 | { 41 | $this->n = $n; 42 | $this->square = $n * $n; 43 | } 44 | 45 | public function even() 46 | { 47 | return $this->n % 2 === 0; 48 | } 49 | } 50 | 51 | class UnderscoreBaseTest extends TestCase 52 | { 53 | public function test_asArray() 54 | { 55 | $this->assertSame(['one'], (new _)->asArray('one')); 56 | $this->assertSame([1, 2], (new _)->asArray([1, 2])); 57 | $this->assertSame(['a', 'b', 'c'], (new _)->asArray(new Stub)); 58 | $this->assertSame(['a', 1, 'c', 3], (new _)->asArray(new _(['a', 1, 'c', 3]))); 59 | $this->assertSame(['a' => 1, 'b' => 2, 'c' => 3], (new _)->asArray(new Json)); 60 | } 61 | 62 | public function test_alias() 63 | { 64 | $this->assertTrue(class_exists('Ahc\Underscore')); 65 | 66 | $this->assertEquals(new \Ahc\Underscore\Underscore, new \Ahc\Underscore); 67 | } 68 | 69 | public function test_underscore() 70 | { 71 | $this->assertTrue(function_exists('underscore')); 72 | 73 | $this->assertInstanceOf(_::class, underscore()); 74 | } 75 | 76 | public function test_now() 77 | { 78 | $this->assertNotEmpty(_::_()->now()); 79 | } 80 | 81 | public function test_keys_values() 82 | { 83 | $array = [[1, 2], 'a' => 3, 7, 'b' => 'B']; 84 | 85 | $this->assertSame(array_keys($array), underscore($array)->keys()->get()); 86 | $this->assertSame(array_values($array), underscore($array)->values()->get()); 87 | } 88 | 89 | public function test_pairs() 90 | { 91 | $array = ['a' => 3, 7, 'b' => 'B']; 92 | 93 | $this->assertSame(['a' => ['a', 3], 0 => [0, 7], 'b' => ['b', 'B']], underscore($array)->pairs()->get()); 94 | } 95 | 96 | public function test_invert() 97 | { 98 | $array = ['a' => 3, 7, 'b' => 'B']; 99 | 100 | $this->assertSame(array_flip($array), underscore($array)->invert()->get()); 101 | } 102 | 103 | public function test_pick_omit() 104 | { 105 | $array = underscore(['a' => 3, 7, 'b' => 'B', 1 => ['c', 5]]); 106 | 107 | $this->assertSame([7, 'b' => 'B'], $array->pick([0, 'b'])->get()); 108 | $this->assertSame(['b' => 'B', 1 => ['c', 5]], $array->pick(1, 'b')->get()); 109 | $this->assertSame(['a' => 3, 7], $array->omit([1, 'b'])->get()); 110 | $this->assertSame(['b' => 'B', 1 => ['c', 5]], $array->omit('a', 0)->get()); 111 | } 112 | 113 | public function test_clone_tap() 114 | { 115 | $main = underscore(['will', 'be', 'cloned']); 116 | $clon = $main->clon(); 117 | 118 | $this->assertNotSame($main, $clon, 'hard equal'); 119 | $this->assertNotSame(spl_object_hash($main), spl_object_hash($clon)); 120 | $this->assertEquals($main, $clon, 'soft equal'); 121 | $this->assertSame($main->toArray(), $clon->toArray()); 122 | 123 | $tap = $main->tap(function ($und) { 124 | return $und->values(); 125 | }); 126 | 127 | $this->assertSame($main, $tap, 'hard equal'); 128 | } 129 | 130 | public function test_mixin() 131 | { 132 | _::mixin('double', function () { 133 | return $this->map(function ($v) { 134 | return $v * 2; 135 | }); 136 | }); 137 | 138 | $und = underscore([10, 20, 30]); 139 | 140 | $this->assertIsCallable([$und, 'double']); 141 | $this->assertSame([20, 40, 60], $und->double()->toArray()); 142 | 143 | $this->expectException(\Ahc\Underscore\UnderscoreException::class); 144 | $this->expectExceptionMessage("The mixin with name 'notMixedIn' is not defined"); 145 | 146 | $und->notMixedIn(); 147 | } 148 | 149 | public function test_valueOf() 150 | { 151 | $this->assertSame('[]', underscore()->valueOf()); 152 | $this->assertSame('[1,2]', underscore([1, 2])->valueOf()); 153 | $this->assertSame('["a","b"]', underscore(['a', 'b'])->valueOf()); 154 | } 155 | 156 | public function test_hom() 157 | { 158 | $i = 10; 159 | 160 | while ($i--) { 161 | $u[] = new Hom($i); 162 | } 163 | 164 | $u = underscore($u); 165 | $sq = $u->filter->even()->map->square->get(); 166 | 167 | // keys are kept intact :) 168 | $this->assertSame([1 => 64, 3 => 36, 5 => 16, 7 => 4, 9 => 0], $sq); 169 | } 170 | 171 | public function test_hom_throws() 172 | { 173 | $this->expectException(\Ahc\Underscore\UnderscoreException::class); 174 | $this->expectExceptionMessage("The 'anon' is not defined"); 175 | 176 | underscore()->anon->value(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/UnderscoreCollectionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | 16 | class UnderscoreCollectionTest extends TestCase 17 | { 18 | public function test_array_json_props() 19 | { 20 | $_ = underscore([9, 'a' => 'Apple', 5, 8, 'c' => 'Cat']); 21 | 22 | $this->assertSame('Apple', $_['a']); 23 | $this->assertSame(8, $_[2]); 24 | $this->assertTrue(isset($_['c'])); 25 | $this->assertFalse(isset($_['D'])); 26 | $this->assertSame(5, $_->size()); 27 | 28 | unset($_['c']); 29 | 30 | $this->assertSame(4, count($_)); 31 | 32 | $_['d'] = 'Dog'; // Set new 33 | $_[0] = 8; // Override 34 | 35 | $this->assertCount(5, $_); 36 | 37 | $json = json_encode($data = [8, 'a' => 'Apple', 5, 8, 'd' => 'Dog']); 38 | $this->assertSame($json, json_encode($_)); 39 | $this->assertSame($json, (string) $_); 40 | 41 | foreach ($_ as $key => $value) { 42 | $this->assertSame($data[$key], $value); 43 | } 44 | } 45 | 46 | /** 47 | * @expectedException \PHPUnit\Framework\Error\Notice 48 | * 49 | * @expectedExceptionMessage Undefined offset: 5 50 | */ 51 | public function test_get() 52 | { 53 | $_ = underscore([1, 5, 9]); 54 | 55 | $this->assertSame([1, 5, 9], $_->get(), 'get all'); 56 | $this->assertSame(5, $_->get(1), 'get by key'); 57 | 58 | $this->assertSame(null, $_->get(5), 'get non existing key'); 59 | } 60 | 61 | public function test_each() 62 | { 63 | $answers = []; 64 | underscore([1, 2, 3])->each(function ($num) use (&$answers) { 65 | $answers[] = $num * 5; 66 | }); 67 | 68 | $this->assertSame([5, 10, 15], $answers, 'callback applied on each member'); 69 | $this->assertCount(3, $answers, 'callback applied exactly 3 times'); 70 | 71 | $answers = []; 72 | underscore(['one' => 1, 'two' => 2, 'three' => 3])->each(function ($num, $index) use (&$answers) { 73 | $answers[] = $index; 74 | }); 75 | 76 | $this->assertSame(['one', 'two', 'three'], $answers, 'callback applied on each member of assoc array'); 77 | } 78 | 79 | public function test_map_collect() 80 | { 81 | $mapped = underscore([1, 2, 3])->map(function ($num) { 82 | return $num * 2; 83 | }); 84 | 85 | $this->assertSame([2, 4, 6], $mapped->get(), 'callback applied on each member'); 86 | 87 | $mapped = underscore([['a' => 1], ['a' => 2]])->collect(function ($row) { 88 | return $row['a']; 89 | }); 90 | 91 | $this->assertSame([1, 2], $mapped->get(), 'map prop'); 92 | } 93 | 94 | public function test_reduce_foldl_inject() 95 | { 96 | $sum = underscore([1, 2, 3])->reduce(function ($sum, $num) { 97 | return $num + $sum; 98 | }, 0); 99 | 100 | $this->assertSame(6, $sum, 'sum by reduce'); 101 | 102 | $sum = underscore([1, 2, 3])->foldl(function ($sum, $num) { 103 | return $num + $sum; 104 | }, 10); 105 | 106 | $this->assertSame(10 + 6, $sum, 'sum by reduce with initial 10'); 107 | 108 | $prod = underscore([1, 2, 3, 4])->inject(function ($prod, $num) { 109 | return $prod * $num; 110 | }, 1); 111 | 112 | $this->assertSame(24, $prod, 'prod by reduce with initial 1'); 113 | 114 | $concat = underscore([1, 2, 3, 4])->inject(function ($concat, $num) { 115 | return $concat . $num; 116 | }, ''); 117 | 118 | $this->assertSame('1234', $concat, 'concat by reduce'); 119 | } 120 | 121 | public function test_reduceRight_foldr() 122 | { 123 | $sum = underscore([1, 2, 3])->reduce(function ($sum, $num) { 124 | return $num + $sum; 125 | }, 0); 126 | 127 | $this->assertSame(6, $sum, 'sum by reduceRight'); 128 | 129 | $concat = underscore([1, 2, 3, 4])->foldr(function ($concat, $num) { 130 | return $concat . $num; 131 | }, ''); 132 | 133 | $this->assertSame('4321', $concat, 'concat by reduceRight'); 134 | } 135 | 136 | public function test_find_detect() 137 | { 138 | $num = underscore([1, 2, 4, 3])->find(function ($num) { 139 | return $num > 2; 140 | }); 141 | 142 | $this->assertSame(4, $num, 'first num gt 2'); 143 | 144 | $num = underscore([1, 2, 3])->detect(function ($num) { 145 | return $num > 4; 146 | }); 147 | 148 | $this->assertNull($num, 'first num gt 5 doesnt exist'); 149 | } 150 | 151 | public function test_filter_select() 152 | { 153 | $gt2 = underscore([1, 2, 4, 0, 3])->filter(function ($num) { 154 | return $num > 2; 155 | }); 156 | 157 | $this->assertSame([4, 3], array_values($gt2->get()), 'nums gt 2'); 158 | 159 | $odds = underscore([1, 2, 3, 4, 5, 7, 6])->select(function ($num) { 160 | return $num % 2 === 1; 161 | }); 162 | 163 | $this->assertSame([1, 3, 5, 7], array_values($odds->get()), 'odd nums'); 164 | } 165 | 166 | public function test_reject() 167 | { 168 | $evens = underscore([1, 2, 3, 4, 5, 7, 6])->reject(function ($num) { 169 | return $num % 2 !== 0; 170 | }); 171 | 172 | $this->assertSame([2, 4, 6], array_values($evens->get()), 'even nums'); 173 | } 174 | 175 | public function test_every_all() 176 | { 177 | $gt0 = underscore([1, 2, 3, 4])->every(function ($num) { 178 | return $num > 0; 179 | }); 180 | 181 | $this->assertTrue($gt0, 'every nums gt 0'); 182 | 183 | $lt0 = underscore([1, 2, 3, 4])->all(function ($num) { 184 | return $num < 0; 185 | }); 186 | 187 | $this->assertFalse($lt0, 'every nums lt 0'); 188 | } 189 | 190 | public function test_some_any() 191 | { 192 | $pos = underscore([1, 2, 0, 4, -1])->some(function ($num) { 193 | return $num > 0; 194 | }); 195 | 196 | $this->assertTrue($pos, 'some positive numbers'); 197 | 198 | $neg = underscore([1, 2, 4])->any(function ($num) { 199 | return $num < 0; 200 | }); 201 | 202 | $this->assertFalse($neg, 'no any neg num'); 203 | } 204 | 205 | public function test_contains_includes() 206 | { 207 | $contains = underscore([1, 2, 4])->contains(2); 208 | 209 | $this->assertTrue($contains, 'contains 2'); 210 | 211 | $includes = underscore([1, 2, 4])->includes(-3); 212 | 213 | $this->assertFalse($includes, 'doesnt include -3'); 214 | } 215 | 216 | public function test_invoke() 217 | { 218 | $sum = underscore([1, 2, 4])->invoke(function () { 219 | return array_sum(func_get_args()); 220 | }); 221 | 222 | $this->assertSame(7, $sum, 'sum items by invoke fn'); 223 | } 224 | 225 | public function test_pluck() 226 | { 227 | $people = underscore([['name' => 'moe', 'age' => 30], ['name' => 'curly']]); 228 | $names = $people->pluck('name')->get(); 229 | $ages = $people->pluck('age')->get(); 230 | 231 | $this->assertSame(['moe', 'curly'], $names, 'pluck names'); 232 | $this->assertSame([30], $ages, 'pluck ages'); 233 | } 234 | 235 | public function test_where() 236 | { 237 | $list = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3]]); 238 | $a1 = $list->where(['a' => 1])->get(); 239 | $a1b2 = $list->where(['a' => 1, 'b' => 2])->get(); 240 | $c3 = $list->where(['c' => 3])->get(); 241 | 242 | $this->assertSame([['a' => 1, 'b' => 2], 2 => ['a' => 1, 'b' => 3]], $a1, 'where a = 1'); 243 | $this->assertSame([['a' => 1, 'b' => 2]], $a1b2, 'where a = 1 and b = 2'); 244 | $this->assertSame([], $c3, 'where c = 3'); 245 | } 246 | 247 | public function test_findWhere() 248 | { 249 | $list = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 2], ['a' => 1, 'b' => 3]]); 250 | $b3 = $list->findWhere(['b' => 3]); 251 | $a2b1 = $list->findWhere(['a' => 2, 'b' => 1]); 252 | 253 | $this->assertSame(['a' => 1, 'b' => 3], $b3, 'findwhere b = 3'); 254 | $this->assertNull($a2b1, 'where a = 2 and b = 1'); 255 | } 256 | 257 | public function test_max_min() 258 | { 259 | $list = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]); 260 | 261 | $this->assertSame(2, $list->max('a'), 'max a = 2'); 262 | $this->assertSame(3, $list->max('b'), 'max a = 3'); 263 | $this->assertSame(0, $list->min('a'), 'min a = 0'); 264 | $this->assertSame(1, $list->min('b'), 'min b = 1'); 265 | 266 | $this->assertSame(5, $list->max(function ($i) { 267 | return $i['a'] + $i['b']; 268 | }), 'max sum of a and b'); 269 | 270 | $this->assertSame(1, $list->min(function ($i) { 271 | return $i['b'] - $i['a']; 272 | }), 'max diff of b and a'); 273 | 274 | $list = underscore([1, 99, 9, -10, 1000, false, 0, 'string', -99, 10000, 87, null]); 275 | 276 | $this->assertSame(10000, $list->max(), 'max = 10000'); 277 | $this->assertSame(-99, $list->min(), 'min = -99'); 278 | } 279 | 280 | public function test_shuffle() 281 | { 282 | $pool = range(1, 5); 283 | $shuf = underscore($pool)->shuffle()->get(); 284 | 285 | foreach ($shuf as $key => $value) { 286 | $this->assertArrayHasKey($key, $pool, 'shuffled item is one from pool'); 287 | $this->assertSame($pool[$key], $value, 'The values are the same as in pool'); 288 | } 289 | 290 | $this->assertSame(count($pool), count($shuf), 'Should have exact counts'); 291 | } 292 | 293 | public function test_sample() 294 | { 295 | $pool = range(10, 5, -1); 296 | 297 | foreach ([1, 2, 3] as $n) { 298 | $samp = underscore($pool)->sample($n)->get(); 299 | 300 | foreach ($samp as $key => $value) { 301 | $this->assertArrayHasKey($key, $pool, 'sampled item is one from pool'); 302 | $this->assertSame($pool[$key], $value, 'The values are the same as in pool'); 303 | } 304 | 305 | $this->assertCount($n, $samp, 'The count should be the one specified'); 306 | } 307 | } 308 | 309 | public function test_sortBy() 310 | { 311 | $sort = $init = range(1, 15); 312 | $sort = underscore($sort)->shuffle()->get(); 313 | 314 | $this->assertNotSame($init, $sort, 'Should be random'); 315 | 316 | $sort = underscore($sort)->sortBy(null)->get(); 317 | 318 | $this->assertSame($init, $sort, 'Should be sorted'); 319 | 320 | $list = underscore([['a' => 1, 'b' => 2], ['a' => 2, 'b' => 3], ['a' => 0, 'b' => 1]]); 321 | 322 | $byA = $list->sortBy('a')->get(); 323 | $this->assertSame( 324 | [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]], 325 | $byA, 326 | 'sort by a' 327 | ); 328 | 329 | $byAB = $list->sortBy(function ($i) { 330 | return $i['a'] + $i['b']; 331 | })->get(); 332 | 333 | $this->assertSame( 334 | [2 => ['a' => 0, 'b' => 1], 0 => ['a' => 1, 'b' => 2], 1 => ['a' => 2, 'b' => 3]], 335 | $byAB, 336 | 'sort by a+b' 337 | ); 338 | } 339 | 340 | public function test_groupBy_indexBy_countBy() 341 | { 342 | $list = underscore([ 343 | ['a' => 0, 'b' => 1, 'c' => 1], 344 | ['a' => true, 'b' => false, 'c' => 'c'], 345 | ['a' => 2, 'b' => 1, 'c' => 2], 346 | ['a' => 1, 'b' => null, 'c' => 0], 347 | ]); 348 | 349 | $grpByA = $list->groupBy('a')->get(); 350 | $idxByA = $list->indexBy('a')->get(); 351 | $cntByA = $list->countBy('a')->get(); 352 | 353 | $this->assertSame([ 354 | 0 => [0 => ['a' => 0, 'b' => 1, 'c' => 1]], 355 | 1 => [1 => ['a' => true, 'b' => false, 'c' => 'c'], 3 => ['a' => 1, 'b' => null, 'c' => 0]], 356 | 2 => [2 => ['a' => 2, 'b' => 1, 'c' => 2]], 357 | ], $grpByA, 'groupBy a'); 358 | 359 | $this->assertSame([ 360 | 0 => ['a' => 0, 'b' => 1, 'c' => 1], 361 | 1 => ['a' => 1, 'b' => null, 'c' => 0], 362 | 2 => ['a' => 2, 'b' => 1, 'c' => 2], 363 | ], $idxByA, 'indexBy a'); 364 | 365 | $this->assertSame([ 366 | 0 => 1, 367 | 1 => 2, 368 | 2 => 1, 369 | ], $cntByA, 'countBy a'); 370 | } 371 | 372 | public function test_toArray() 373 | { 374 | $array = [['deep' => 1, 'ok'], 'shallow', 0, false]; 375 | 376 | $this->assertSame($array, underscore($array)->toArray()); 377 | } 378 | 379 | public function test_partition() 380 | { 381 | $nums = underscore(range(1, 10)); 382 | $oddEvn = $nums->partition(function ($i) { 383 | return $i % 2; 384 | })->get(); 385 | $evnOdd = $nums->partition(function ($i) { 386 | return $i % 2 == 0; 387 | })->get(); 388 | 389 | $this->assertCount(2, $oddEvn, '2 partitions'); 390 | $this->assertArrayHasKey(0, $oddEvn, 'odd partition'); 391 | $this->assertArrayHasKey(1, $oddEvn, 'even partition'); 392 | 393 | $this->assertSame([1, 3, 5, 7, 9], $oddEvn[0], 'odds'); 394 | $this->assertSame([1, 3, 5, 7, 9], $oddEvn[0], 'odds'); 395 | $this->assertSame($evnOdd[1], $oddEvn[0], 'odds crosscheck'); 396 | $this->assertSame($evnOdd[0], $oddEvn[1], 'evens crosscheck'); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /tests/UnderscoreFunctionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Tests; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | 16 | class UnderscoreFunctionTest extends TestCase 17 | { 18 | public function test_memoize() 19 | { 20 | $memoSum = underscore()->memoize(function ($a, $b) { 21 | echo "sum $a + $b"; 22 | 23 | return $a + $b; 24 | }); 25 | 26 | // Every time the sum callback is called it echoes something. 27 | // But since it is memorized, it should only echo in the first call. 28 | ob_start(); 29 | 30 | // Call 3 times! 31 | $this->assertSame(1 + 2, $memoSum(1, 2)); 32 | $this->assertSame(1 + 2, $memoSum(1, 2)); 33 | $this->assertSame(1 + 2, $memoSum(1, 2)); 34 | 35 | // Call twice for different args! 36 | $this->assertSame(3 + 2, $memoSum(3, 2)); 37 | $this->assertSame(3 + 2, $memoSum(3, 2)); 38 | 39 | $buffer = ob_get_clean(); 40 | 41 | $this->assertSame( 42 | 1, 43 | substr_count($buffer, 'sum 1 + 2'), 44 | 'Should be called only once, subsequent calls uses memo' 45 | ); 46 | $this->assertSame( 47 | 1, 48 | substr_count($buffer, 'sum 3 + 2'), 49 | 'Should be called only once, subsequent calls uses memo' 50 | ); 51 | } 52 | 53 | public function test_delay() 54 | { 55 | $callback = function () { 56 | // Do nothing! 57 | }; 58 | 59 | // Calibrate time taken by callback! 60 | $cTime = microtime(1); 61 | $callback(); 62 | $cTime = microtime(1) - $cTime; 63 | 64 | // Now delay this callback by 10millis (0.01sec). 65 | $delayCall = underscore()->delay($callback, 10); 66 | 67 | $time = microtime(1); 68 | $delayCall(); 69 | $time = microtime(1) - $time; 70 | 71 | // The overall time must be >= (cTime + 1sec). 72 | $this->assertGreaterThanOrEqual(0.01 + $cTime, $time); 73 | } 74 | 75 | public function test_throttle() 76 | { 77 | $callback = function () { 78 | echo 'throttle'; 79 | }; 80 | 81 | // Throttle the call for once per 10millis (0.01 sec) 82 | // So that for a period of 300millis it should be actually called at most 3 times. 83 | $throtCall = underscore()->throttle($callback, 10); 84 | 85 | ob_start(); 86 | 87 | $start = microtime(1); 88 | while (microtime(1) - $start <= 0.031) { 89 | $throtCall(); 90 | } 91 | 92 | $buffer = ob_get_clean(); 93 | 94 | $this->assertLessThanOrEqual( 95 | 3, 96 | substr_count($buffer, 'throttle'), 97 | 'Should be called only once, subsequent calls uses memo' 98 | ); 99 | } 100 | 101 | public function test_compose() 102 | { 103 | $c = underscore()->compose('strlen', 'strtolower', 'strtoupper'); 104 | 105 | $this->assertSame(7, $c('aBc.xYz')); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/UnderscoreTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | namespace Ahc\Underscore\Tests; 13 | 14 | use Ahc\Underscore\Underscore as _; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class UnderscoreTest extends TestCase 18 | { 19 | public function test_constant() 20 | { 21 | foreach ([1, 'A', [], new \stdClass()] as $value) { 22 | $fn = underscore()->constant($value); 23 | 24 | $this->assertSame($value, $fn()); 25 | } 26 | } 27 | 28 | public function test_() 29 | { 30 | $this->assertInstanceOf(_::class, _::_()); 31 | } 32 | 33 | public function test_noop() 34 | { 35 | $this->markTestSkipped('is buggy'); 36 | 37 | $epsilon = 0.0000000001; 38 | 39 | $t = microtime(1); 40 | $m = memory_get_usage(); 41 | $x = underscore()->noop(); 42 | $t = microtime(1) - $t; 43 | $m = memory_get_usage() - $m; 44 | 45 | $this->assertLessThanOrEqual($t, $epsilon); 46 | $this->assertLessThanOrEqual($m, $epsilon); 47 | } 48 | 49 | public function test_times() 50 | { 51 | $fn = function ($i) { 52 | return $i * 2; 53 | }; 54 | 55 | $o = underscore()->times(5, $fn); 56 | 57 | $this->assertSame([0, 2, 4, 6, 8], $o->toArray()); 58 | } 59 | 60 | public function test_random() 61 | { 62 | $i = 10; 63 | 64 | while ($i--) { 65 | $cases[rand(1, 10)] = rand(11, 20); 66 | } 67 | 68 | foreach ($cases as $l => $r) { 69 | $rand = underscore()->random($l, $r); 70 | 71 | $this->assertGreaterThanOrEqual($l, $rand); 72 | $this->assertLessThanOrEqual($r, $rand); 73 | } 74 | } 75 | 76 | public function test_unique_id() 77 | { 78 | $u = underscore()->uniqueId(); 79 | $u1 = underscore()->uniqueId(); 80 | $u3 = underscore()->uniqueId('id:'); 81 | 82 | $this->assertSame('1', $u); 83 | $this->assertSame('2', $u1); 84 | $this->assertSame('id:3', $u3); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * 9 | * Licensed under MIT license. 10 | */ 11 | 12 | require_once __DIR__ . '/../vendor/autoload.php'; 13 | 14 | if (class_exists('\PHPUnit_Framework_Error_Notice')) { 15 | class_alias('\PHPUnit_Framework_Error_Notice', '\PHPUnit\Framework\Error\Notice'); 16 | } 17 | --------------------------------------------------------------------------------