├── .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 | [](https://github.com/adhocore/php-underscore/releases)
6 | [](https://travis-ci.org/adhocore/php-underscore?branch=master)
7 | [](https://scrutinizer-ci.com/g/adhocore/php-underscore/?branch=master)
8 | [](https://codecov.io/gh/adhocore/php-underscore)
9 | [](https://styleci.io/repos/108437038)
10 | [](LICENSE)
11 | [](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 | [](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 |
--------------------------------------------------------------------------------