• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aimeos / map / 6783666054

07 Nov 2023 11:10AM UTC coverage: 97.911% (+0.03%) from 97.881%
6783666054

push

github

aimeos
Merge branch 'master' into 3.x

703 of 718 relevant lines covered (97.91%)

43.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.9
/src/Map.php
1
<?php
2

3
/**
4
 * @license MIT, http://opensource.org/licenses/MIT
5
 * @author Taylor Otwell, Aimeos.org developers
6
 */
7

8

9
namespace Aimeos;
10

11

12
/**
13
 * Handling and operating on a list of elements easily
14
 * Inspired by Laravel Collection class, PHP map data structure and Javascript
15
 *
16
 * @template-implements \ArrayAccess<int|string,mixed>
17
 * @template-implements \IteratorAggregate<int|string,mixed>
18
 */
19
class Map implements \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable
20
{
21
        /**
22
         * @var array<string,\Closure>
23
         */
24
        protected static $methods = [];
25

26
        /**
27
         * @var string
28
         */
29
        protected static $delim = '/';
30

31
        /**
32
         * @var array<int|string,mixed>|\Closure|iterable|mixed
33
         */
34
        protected $list;
35

36
        /**
37
         * @var string
38
         */
39
        protected $sep = '/';
40

41

42
        /**
43
         * Creates a new map.
44
         *
45
         * Returns a new map instance containing the list of elements. In case of
46
         * an empty array or null, the map object will contain an empty list.
47
         *
48
         * @param mixed $elements List of elements or single value
49
         */
50
        public function __construct( $elements = [] )
51
        {
52
                $this->sep = self::$delim;
2,506✔
53
                $this->list = $elements;
2,506✔
54
        }
716✔
55

56

57
        /**
58
         * Handles static calls to custom methods for the class.
59
         *
60
         * Calls a custom method added by Map::method() statically. The called method
61
         * has no access to the internal array because no object is available.
62
         *
63
         * Examples:
64
         *  Map::method( 'foo', function( $arg1, $arg2 ) {} );
65
         *  Map::foo( $arg1, $arg2 );
66
         *
67
         * @param string $name Method name
68
         * @param array<mixed> $params List of parameters
69
         * @return mixed Result from called function or new map with results from the element methods
70
         *
71
         * @throws \BadMethodCallException
72
         */
73
        public static function __callStatic( string $name, array $params )
74
        {
75
                if( !isset( static::$methods[$name] ) ) {
14✔
76
                        throw new \BadMethodCallException( sprintf( 'Method %s::%s does not exist.', static::class, $name ) );
7✔
77
                }
78

79
                return call_user_func_array( \Closure::bind( static::$methods[$name], null, static::class ), $params );
7✔
80
        }
81

82

83
        /**
84
         * Handles dynamic calls to custom methods for the class.
85
         *
86
         * Calls a custom method added by Map::method(). The called method
87
         * has access to the internal array by using $this->list().
88
         *
89
         * Examples:
90
         *  Map::method( 'case', function( $case = CASE_LOWER ) {
91
         *      return new static( array_change_key_case( $this->list(), $case ) );
92
         *  } );
93
         *  Map::from( ['a' => 'bar'] )->case( CASE_UPPER );
94
         *
95
         *  $item = new MyClass(); // with method setId() and getCode()
96
         *  Map::from( [$item, $item] )->setId( null )->getCode();
97
         *
98
         * Results:
99
         * The first example will return ['A' => 'bar'].
100
         *
101
         * The second one will call the setId() method of each element in the map and use
102
         * their return values to create a new map. On the new map, the getCode() method
103
         * is called for every element and its return values are also stored in a new map.
104
         * This last map is then returned.
105
         * If this applies to all elements, an empty map is returned. The map keys from the
106
         * original map are preserved in the returned map.
107
         *
108
         * @param string $name Method name
109
         * @param array<mixed> $params List of parameters
110
         * @return mixed|self Result from called function or new map with results from the element methods
111
         */
112
        public function __call( string $name, array $params )
113
        {
114
                if( isset( static::$methods[$name] ) ) {
28✔
115
                        return call_user_func_array( static::$methods[$name]->bindTo( $this, static::class ), $params );
14✔
116
                }
117

118
                $result = [];
14✔
119

120
                foreach( $this->list() as $key => $item )
14✔
121
                {
122
                        if( is_object( $item ) ) {
7✔
123
                                $result[$key] = $item->{$name}( ...$params );
7✔
124
                        }
125
                }
126

127
                return new static( $result );
14✔
128
        }
129

130

131
        /**
132
         * Returns the elements as a plain array.
133
         *
134
         * @return array<int|string,mixed> Plain array
135
         */
136
        public function __toArray() : array
137
        {
138
                return $this->list = $this->array( $this->list );
7✔
139
        }
140

141

142
        /**
143
         * Sets or returns the seperator for paths to values in multi-dimensional arrays or objects.
144
         *
145
         * The static method only changes the separator for new maps created afterwards.
146
         * Already existing maps will continue to use the previous separator. To change
147
         * the separator of an existing map, use the sep() method instead.
148
         *
149
         * Examples:
150
         *  Map::delimiter( '.' );
151
         *  Map::from( ['foo' => ['bar' => 'baz']] )->get( 'foo.bar' );
152
         *
153
         * Results:
154
         *  '/'
155
         *  'baz'
156
         *
157
         * @param string|null $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
158
         * @return string Separator used up to now
159
         */
160
        public static function delimiter( ?string $char = null ) : string
161
        {
162
                $old = self::$delim;
7✔
163

164
                if( $char ) {
7✔
165
                        self::$delim = $char;
7✔
166
                }
167

168
                return $old;
7✔
169
        }
170

171

172
        /**
173
         * Creates a new map with the string splitted by the delimiter.
174
         *
175
         * This method creates a lazy Map and the string is split after calling
176
         * another method that operates on the Map contents.
177
         *
178
         * Examples:
179
         *  Map::explode( ',', 'a,b,c' );
180
         *  Map::explode( '<-->', 'a a<-->b b<-->c c' );
181
         *  Map::explode( '', 'string' );
182
         *  Map::explode( '|', 'a|b|c', 2 );
183
         *  Map::explode( '', 'string', 2 );
184
         *  Map::explode( '|', 'a|b|c|d', -2 );
185
         *  Map::explode( '', 'string', -3 );
186
         *
187
         * Results:
188
         *  ['a', 'b', 'c']
189
         *  ['a a', 'b b', 'c c']
190
         *  ['s', 't', 'r', 'i', 'n', 'g']
191
         *  ['a', 'b|c']
192
         *  ['s', 't', 'ring']
193
         *  ['a', 'b']
194
         *  ['s', 't', 'r']
195
         *
196
         * A limit of "0" is treated the same as "1". If limit is negative, the rest of
197
         * the string is dropped and not part of the returned map.
198
         *
199
         * @param string $delimiter Delimiter character, string or empty string
200
         * @param string $string String to split
201
         * @param int $limit Maximum number of element with the last element containing the rest of the string
202
         * @return self<int|string,mixed> New map with splitted parts
203
         */
204
        public static function explode( string $delimiter, string $string, int $limit = PHP_INT_MAX ) : self
205
        {
206
                return new static( function() use ( $delimiter, $string, $limit ) {
40✔
207

208
                        if( $delimiter !== '' ) {
56✔
209
                                return explode( $delimiter, $string, $limit );
28✔
210
                        }
211

212
                        $limit = $limit ?: 1;
28✔
213
                        $parts = mb_str_split( $string );
28✔
214

215
                        if( $limit < 1 ) {
28✔
216
                                return array_slice( $parts, 0, $limit );
7✔
217
                        }
218

219
                        if( $limit < count( $parts ) )
21✔
220
                        {
221
                                $result = array_slice( $parts, 0, $limit );
7✔
222
                                $result[] = join( '', array_slice( $parts, $limit ) );
7✔
223
                                return $result;
7✔
224
                        }
225

226
                        return $parts;
14✔
227
                } );
56✔
228
        }
229

230

231
        /**
232
         * Creates a new map instance if the value isn't one already.
233
         *
234
         * Examples:
235
         *  Map::from( [] );
236
         *  Map::from( null );
237
         *  Map::from( 'a' );
238
         *  Map::from( new Map() );
239
         *  Map::from( new ArrayObject() );
240
         *
241
         * Results:
242
         * A new map instance containing the list of elements. In case of an empty
243
         * array or null, the map object will contain an empty list. If a map object
244
         * is passed, it will be returned instead of creating a new instance.
245
         *
246
         * @param mixed $elements List of elements or single element
247
         * @return self<int|string,mixed> New map object
248
         */
249
        public static function from( $elements = [] ) : self
250
        {
251
                if( $elements instanceof self ) {
1,148✔
252
                        return $elements;
21✔
253
                }
254

255
                return new static( $elements );
1,148✔
256
        }
257

258

259
        /**
260
         * Creates a new map instance from a JSON string.
261
         *
262
         * This method creates a lazy Map and the string is decoded after calling
263
         * another method that operates on the Map contents. Thus, the exception in
264
         * case of an error isn't thrown immediately but after calling the next method.
265
         *
266
         * Examples:
267
         *  Map::fromJson( '["a", "b"]' );
268
         *  Map::fromJson( '{"a": "b"}' );
269
         *  Map::fromJson( '""' );
270
         *
271
         * Results:
272
         *  ['a', 'b']
273
         *  ['a' => 'b']
274
         *  ['']
275
         *
276
         * There are several options available for decoding the JSON string:
277
         * {@link https://www.php.net/manual/en/function.json-decode.php}
278
         * The parameter can be a single JSON_* constant or a bitmask of several
279
         * constants combine by bitwise OR (|), e.g.:
280
         *
281
         *  JSON_BIGINT_AS_STRING|JSON_INVALID_UTF8_IGNORE
282
         *
283
         * @param int $options Combination of JSON_* constants
284
         * @return self<int|string,mixed> New map from decoded JSON string
285
         * @throws \RuntimeException If the passed JSON string is invalid
286
         */
287
        public static function fromJson( string $json, int $options = JSON_BIGINT_AS_STRING ) : self
288
        {
289
                return new static( function() use ( $json, $options ) {
20✔
290

291
                        if( ( $result = json_decode( $json, true, 512, $options ) ) !== null ) {
28✔
292
                                return $result;
21✔
293
                        }
294

295
                        throw new \RuntimeException( 'Not a valid JSON string: ' . $json );
7✔
296
                } );
28✔
297
        }
298

299

300
        /**
301
         * Registers a custom method or returns the existing one.
302
         *
303
         * The registed method has access to the class properties if called non-static.
304
         *
305
         * Examples:
306
         *  Map::method( 'foo', function( $arg1, $arg2 ) {
307
         *      return $this->list();
308
         *  } );
309
         *
310
         * Dynamic calls have access to the class properties:
311
         *  Map::from( ['bar'] )->foo( $arg1, $arg2 );
312
         *
313
         * Static calls yield an error because $this->elements isn't available:
314
         *  Map::foo( $arg1, $arg2 );
315
         *
316
         * @param string $method Method name
317
         * @param \Closure|null $fcn Anonymous function or NULL to return the closure if available
318
         * @return \Closure|null Registered anonymous function or NULL if none has been registered
319
         */
320
        public static function method( string $method, \Closure $fcn = null ) : ?\Closure
321
        {
322
                if( $fcn ) {
21✔
323
                        self::$methods[$method] = $fcn;
21✔
324
                }
325

326
                return self::$methods[$method] ?? null;
21✔
327
        }
328

329

330
        /**
331
         * Creates a new map by invoking the closure the given number of times.
332
         *
333
         * This method creates a lazy Map and the entries are generated after calling
334
         * another method that operates on the Map contents. Thus, the passed callback
335
         * is not called immediately!
336
         *
337
         * Examples:
338
         *  Map::times( 3, function( $num ) {
339
         *    return $num * 10;
340
         *  } );
341
         *  Map::times( 3, function( $num, &$key ) {
342
         *    $key = $num * 2;
343
         *    return $num * 5;
344
         *  } );
345
         *  Map::times( 2, function( $num ) {
346
         *    return new \stdClass();
347
         *  } );
348
         *
349
         * Results:
350
         *  [0 => 0, 1 => 10, 2 => 20]
351
         *  [0 => 0, 2 => 5, 4 => 10]
352
         *  [0 => new \stdClass(), 1 => new \stdClass()]
353
         *
354
         * @param int $num Number of times the function is called
355
         * @param \Closure $callback Function with (value, key) parameters and returns new value
356
         * @return self<int|string,mixed> New map with the generated elements
357
         */
358
        public static function times( int $num, \Closure $callback ) : self
359
        {
360
                return new static( function() use ( $num, $callback ) {
15✔
361

362
                        $list = [];
21✔
363

364
                        for( $i = 0; $i < $num; $i++ ) {
21✔
365
                                $key = $i;
21✔
366
                                $list[$key] = $callback( $i, $key );
21✔
367
                        }
368

369
                        return $list;
21✔
370
                } );
21✔
371
        }
372

373

374
        /**
375
         * Returns the elements after the given one.
376
         *
377
         * Examples:
378
         *  Map::from( ['a' => 1, 'b' => 0] )->after( 1 );
379
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'b' );
380
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'c' );
381
         *  Map::from( ['a', 'c', 'b'] )->after( function( $item, $key ) {
382
         *      return $item >= 'c';
383
         *  } );
384
         *
385
         * Results:
386
         *  ['b' => 0]
387
         *  [1 => 'a']
388
         *  []
389
         *  [2 => 'b']
390
         *
391
         * The keys are preserved using this method.
392
         *
393
         * @param \Closure|int|string $value Value or function with (item, key) parameters
394
         * @return self<int|string,mixed> New map with the elements after the given one
395
         */
396
        public function after( $value ) : self
397
        {
398
                if( ( $pos = $this->pos( $value ) ) === null ) {
28✔
399
                        return new static();
7✔
400
                }
401

402
                return new static( array_slice( $this->list(), $pos + 1, null, true ) );
21✔
403
        }
404

405

406
        /**
407
         * Returns the elements as a plain array.
408
         *
409
         * @return array<int|string,mixed> Plain array
410
         */
411
        public function all() : array
412
        {
413
                return $this->list = $this->array( $this->list );
105✔
414
        }
415

416

417
        /**
418
         * Sorts all elements in reverse order and maintains the key association.
419
         *
420
         * Examples:
421
         *  Map::from( ['b' => 0, 'a' => 1] )->arsort();
422
         *  Map::from( ['a', 'b'] )->arsort();
423
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort();
424
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
425
         *
426
         * Results:
427
         *  ['a' => 1, 'b' => 0]
428
         *  ['b', 'a']
429
         *  [1 => 'b', 0 => 'C']
430
         *  [0 => 'C', 1 => 'b'] // because 'C' -> 'c' and 'c' > 'b'
431
         *
432
         * The parameter modifies how the values are compared. Possible parameter values are:
433
         * - SORT_REGULAR : compare elements normally (don't change types)
434
         * - SORT_NUMERIC : compare elements numerically
435
         * - SORT_STRING : compare elements as strings
436
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
437
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
438
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
439
         *
440
         * The keys are preserved using this method and no new map is created.
441
         *
442
         * @param int $options Sort options for arsort()
443
         * @return self<int|string,mixed> Updated map for fluid interface
444
         */
445
        public function arsort( int $options = SORT_REGULAR ) : self
446
        {
447
                arsort( $this->list(), $options );
21✔
448
                return $this;
21✔
449
        }
450

451

452
        /**
453
         * Sorts all elements and maintains the key association.
454
         *
455
         * Examples:
456
         *  Map::from( ['a' => 1, 'b' => 0] )->asort();
457
         *  Map::from( [0 => 'b', 1 => 'a'] )->asort();
458
         *  Map::from( [0 => 'C', 1 => 'b'] )->asort();
459
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
460
         *
461
         * Results:
462
         *  ['b' => 0, 'a' => 1]
463
         *  [1 => 'a', 0 => 'b']
464
         *  [0 => 'C', 1 => 'b'] // because 'C' < 'b'
465
         *  [1 => 'b', 0 => 'C'] // because 'C' -> 'c' and 'c' > 'b'
466
         *
467
         * The parameter modifies how the values are compared. Possible parameter values are:
468
         * - SORT_REGULAR : compare elements normally (don't change types)
469
         * - SORT_NUMERIC : compare elements numerically
470
         * - SORT_STRING : compare elements as strings
471
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
472
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
473
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
474
         *
475
         * The keys are preserved using this method and no new map is created.
476
         *
477
         * @param int $options Sort options for asort()
478
         * @return self<int|string,mixed> Updated map for fluid interface
479
         */
480
        public function asort( int $options = SORT_REGULAR ) : self
481
        {
482
                asort( $this->list(), $options );
21✔
483
                return $this;
21✔
484
        }
485

486

487
        /**
488
         * Returns the value at the given position.
489
         *
490
         * Examples:
491
         *  Map::from( [1, 3, 5] )->at( 0 );
492
         *  Map::from( [1, 3, 5] )->at( 1 );
493
         *  Map::from( [1, 3, 5] )->at( -1 );
494
         *  Map::from( [1, 3, 5] )->at( 3 );
495
         *
496
         * Results:
497
         * The first line will return "1", the second one "3", the third one "5" and
498
         * the last one NULL.
499
         *
500
         * The position starts from zero and a position of "0" returns the first element
501
         * of the map, "1" the second and so on. If the position is negative, the
502
         * sequence will start from the end of the map.
503
         *
504
         * @param int $pos Position of the value in the map
505
         * @return mixed|null Value at the given position or NULL if no value is available
506
         */
507
        public function at( int $pos )
508
        {
509
                $pair = array_slice( $this->list(), $pos, 1 );
7✔
510
                return !empty( $pair ) ? current( $pair ) : null;
7✔
511
        }
512

513

514
        /**
515
         * Returns the average of all integer and float values in the map.
516
         *
517
         * Examples:
518
         *  Map::from( [1, 3, 5] )->avg();
519
         *  Map::from( [1, null, 5] )->avg();
520
         *  Map::from( [1, 'sum', 5] )->avg();
521
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->avg( 'p' );
522
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->avg( 'i/p' );
523
         *
524
         * Results:
525
         * The first line will return "3", the second and third one "2", the forth
526
         * one "30" and the last one "40".
527
         *
528
         * This does also work for multi-dimensional arrays by passing the keys
529
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
530
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
531
         * public properties of objects or objects implementing __isset() and __get() methods.
532
         *
533
         * @param string|null $key Key or path to the values in the nested array or object to compute the average for
534
         * @return float Average of all elements or 0 if there are no elements in the map
535
         */
536
        public function avg( string $key = null ) : float
537
        {
538
                $cnt = count( $this->list() );
14✔
539
                return $cnt > 0 ? $this->sum( $key ) / $cnt : 0;
14✔
540
        }
541

542

543
        /**
544
         * Returns the elements before the given one.
545
         *
546
         * Examples:
547
         *  Map::from( ['a' => 1, 'b' => 0] )->before( 0 );
548
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'a' );
549
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'b' );
550
         *  Map::from( ['a', 'c', 'b'] )->before( function( $item, $key ) {
551
         *      return $key >= 1;
552
         *  } );
553
         *
554
         * Results:
555
         *  ['a' => 1]
556
         *  [0 => 'b']
557
         *  []
558
         *  [0 => 'a']
559
         *
560
         * The keys are preserved using this method.
561
         *
562
         * @param \Closure|int|string $value Value or function with (item, key) parameters
563
         * @return self<int|string,mixed> New map with the elements before the given one
564
         */
565
        public function before( $value ) : self
566
        {
567
                return new static( array_slice( $this->list(), 0, $this->pos( $value ), true ) );
28✔
568
        }
569

570

571
        /**
572
         * Returns an element by key and casts it to boolean if possible.
573
         *
574
         * Examples:
575
         *  Map::from( ['a' => true] )->bool( 'a' );
576
         *  Map::from( ['a' => '1'] )->bool( 'a' );
577
         *  Map::from( ['a' => 1.1] )->bool( 'a' );
578
         *  Map::from( ['a' => '10'] )->bool( 'a' );
579
         *  Map::from( ['a' => 'abc'] )->bool( 'a' );
580
         *  Map::from( ['a' => ['b' => ['c' => true]]] )->bool( 'a/b/c' );
581
         *  Map::from( [] )->bool( 'c', function() { return rand( 1, 2 ); } );
582
         *  Map::from( [] )->bool( 'a', true );
583
         *
584
         *  Map::from( [] )->bool( 'b' );
585
         *  Map::from( ['b' => ''] )->bool( 'b' );
586
         *  Map::from( ['b' => null] )->bool( 'b' );
587
         *  Map::from( ['b' => [true]] )->bool( 'b' );
588
         *  Map::from( ['b' => resource] )->bool( 'b' );
589
         *  Map::from( ['b' => new \stdClass] )->bool( 'b' );
590
         *
591
         *  Map::from( [] )->bool( 'c', new \Exception( 'error' ) );
592
         *
593
         * Results:
594
         * The first eight examples will return TRUE while the 9th to 14th example
595
         * returns FALSE. The last example will throw an exception.
596
         *
597
         * This does also work for multi-dimensional arrays by passing the keys
598
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
599
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
600
         * public properties of objects or objects implementing __isset() and __get() methods.
601
         *
602
         * @param int|string $key Key or path to the requested item
603
         * @param mixed $default Default value if key isn't found (will be casted to bool)
604
         * @return bool Value from map or default value
605
         */
606
        public function bool( $key, $default = false ) : bool
607
        {
608
                return (bool) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
21✔
609
        }
610

611

612
        /**
613
         * Calls the given method on all items and returns the result.
614
         *
615
         * This method can call methods on the map entries that are also implemented
616
         * by the map object itself and are therefore not reachable when using the
617
         * magic __call() method.
618
         *
619
         * Examples:
620
         *  $item = new MyClass(); // implements methods get() and toArray()
621
         *  Map::from( [$item, $item] )->call( 'get', ['myprop'] );
622
         *  Map::from( [$item, $item] )->call( 'toArray' );
623
         *
624
         * Results:
625
         * The first example will return ['...', '...'] while the second one returns [[...], [...]].
626
         *
627
         * If some entries are not objects, they will be skipped. The map keys from the
628
         * original map are preserved in the returned map.
629
         *
630
         * @param string $name Method name
631
         * @param array<mixed> $params List of parameters
632
         * @return self<int|string,mixed> New map with results from all elements
633
         */
634
        public function call( string $name, array $params = [] ) : self
635
        {
636
                $result = [];
7✔
637

638
                foreach( $this->list() as $key => $item )
7✔
639
                {
640
                        if( is_object( $item ) ) {
7✔
641
                                $result[$key] = $item->{$name}( ...$params );
7✔
642
                        }
643
                }
644

645
                return new static( $result );
7✔
646
        }
647

648

649
        /**
650
         * Casts all entries to the passed type.
651
         *
652
         * Examples:
653
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast();
654
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'bool' );
655
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'int' );
656
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'float' );
657
         *  Map::from( [new stdClass, new stdClass] )->cast( 'array' );
658
         *  Map::from( [[], []] )->cast( 'object' );
659
         *
660
         * Results:
661
         * The examples will return (in this order):
662
         * ['1', '1', '1.0', 'yes']
663
         * [true, true, true, true]
664
         * [1, 1, 1, 0]
665
         * [1.0, 1.0, 1.0, 0.0]
666
         * [[], []]
667
         * [new stdClass, new stdClass]
668
         *
669
         * Casting arrays and objects to scalar values won't return anything useful!
670
         *
671
         * @param string $type Type to cast the values to ("string", "bool", "int", "float", "array", "object")
672
         * @return self<int|string,mixed> Updated map with casted elements
673
         */
674
        public function cast( string $type = 'string' ) : self
675
        {
676
                foreach( $this->list() as &$item )
7✔
677
                {
678
                        switch( $type )
1✔
679
                        {
680
                                case 'bool': $item = (bool) $item; break;
7✔
681
                                case 'int': $item = (int) $item; break;
7✔
682
                                case 'float': $item = (float) $item; break;
7✔
683
                                case 'string': $item = (string) $item; break;
7✔
684
                                case 'array': $item = (array) $item; break;
7✔
685
                                case 'object': $item = (object) $item; break;
7✔
686
                        }
687
                }
688

689
                return $this;
7✔
690
        }
691

692

693
        /**
694
         * Chunks the map into arrays with the given number of elements.
695
         *
696
         * Examples:
697
         *  Map::from( [0, 1, 2, 3, 4] )->chunk( 3 );
698
         *  Map::from( ['a' => 0, 'b' => 1, 'c' => 2] )->chunk( 2 );
699
         *
700
         * Results:
701
         *  [[0, 1, 2], [3, 4]]
702
         *  [['a' => 0, 'b' => 1], ['c' => 2]]
703
         *
704
         * The last chunk may contain less elements than the given number.
705
         *
706
         * The sub-arrays of the returned map are plain PHP arrays. If you need Map
707
         * objects, then wrap them with Map::from() when you iterate over the map.
708
         *
709
         * @param int $size Maximum size of the sub-arrays
710
         * @param bool $preserve Preserve keys in new map
711
         * @return self<int|string,mixed> New map with elements chunked in sub-arrays
712
         * @throws \InvalidArgumentException If size is smaller than 1
713
         */
714
        public function chunk( int $size, bool $preserve = false ) : self
715
        {
716
                if( $size < 1 ) {
21✔
717
                        throw new \InvalidArgumentException( 'Chunk size must be greater or equal than 1' );
7✔
718
                }
719

720
                return new static( array_chunk( $this->list(), $size, $preserve ) );
14✔
721
        }
722

723

724
        /**
725
         * Removes all elements from the current map.
726
         *
727
         * @return self<int|string,mixed> Updated map for fluid interface
728
         */
729
        public function clear() : self
730
        {
731
                $this->list = [];
28✔
732
                return $this;
28✔
733
        }
734

735

736
        /**
737
         * Clones the map and all objects within.
738
         *
739
         * Examples:
740
         *  Map::from( [new \stdClass, new \stdClass] )->clone();
741
         *
742
         * Results:
743
         *   [new \stdClass, new \stdClass]
744
         *
745
         * The objects within the Map are NOT the same as before but new cloned objects.
746
         * This is different to copy(), which doesn't clone the objects within.
747
         *
748
         * The keys are preserved using this method.
749
         *
750
         * @return self<int|string,mixed> New map with cloned objects
751
         */
752
        public function clone() : self
753
        {
754
                $list = [];
7✔
755

756
                foreach( $this->list() as $key => $item ) {
7✔
757
                        $list[$key] = is_object( $item ) ? clone $item : $item;
7✔
758
                }
759

760
                return new static( $list );
7✔
761
        }
762

763

764
        /**
765
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
766
         *
767
         * Examples:
768
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val' );
769
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val', 'id' );
770
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( null, 'id' );
771
         *  Map::from( [['id' => 'ix', 'val' => 'v1'], ['id' => 'ix', 'val' => 'v2']] )->col( null, 'id' );
772
         *  Map::from( [['foo' => ['bar' => 'one', 'baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
773
         *  Map::from( [['foo' => ['bar' => 'one']]] )->col( 'foo/baz', 'foo/bar' );
774
         *  Map::from( [['foo' => ['baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
775
         *
776
         * Results:
777
         *  ['v1', 'v2']
778
         *  ['i1' => 'v1', 'i2' => 'v2']
779
         *  ['i1' => ['id' => 'i1', 'val' => 'v1'], 'i2' => ['id' => 'i2', 'val' => 'v2']]
780
         *  ['ix' => ['id' => 'ix', 'val' => 'v2']]
781
         *  ['one' => 'two']
782
         *  ['one' => null]
783
         *  ['two']
784
         *
785
         * If $indexcol is omitted, it's value is NULL or not set, the result will be indexed from 0-n.
786
         * Items with the same value for $indexcol will overwrite previous items and only the last
787
         * one will be part of the resulting map.
788
         *
789
         * This does also work to map values from multi-dimensional arrays by passing the keys
790
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
791
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
792
         * public properties of objects or objects implementing __isset() and __get() methods.
793
         *
794
         * @param string|null $valuecol Name or path of the value property
795
         * @param string|null $indexcol Name or path of the index property
796
         * @return self<int|string,mixed> New map with mapped entries
797
         */
798
        public function col( string $valuecol = null, string $indexcol = null ) : self
799
        {
800
                $vparts = explode( $this->sep, (string) $valuecol );
119✔
801
                $iparts = explode( $this->sep, (string) $indexcol );
119✔
802

803
                if( count( $vparts ) === 1 && count( $iparts ) === 1 ) {
119✔
804
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
84✔
805
                }
806

807
                $list = [];
63✔
808

809
                foreach( $this->list() as $item )
63✔
810
                {
811
                        $v = $valuecol ? $this->val( $item, $vparts ) : $item;
63✔
812

813
                        if( $indexcol !== null && ( $key = $this->val( $item, $iparts ) ) !== null ) {
63✔
814
                                $list[(string) $key] = $v;
21✔
815
                        } else {
816
                                $list[] = $v;
45✔
817
                        }
818
                }
819

820
                return new static( $list );
63✔
821
        }
822

823

824
        /**
825
         * Collapses all sub-array elements recursively to a new map overwriting existing keys.
826
         *
827
         * Examples:
828
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['c' => 2, 'd' => 3]] )->collapse();
829
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['a' => 2]] )->collapse();
830
         *  Map::from( [0 => [0 => 0, 1 => 1], 1 => [0 => ['a' => 2, 0 => 3], 1 => 4]] )->collapse();
831
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => [0 => ['b' => 2, 0 => 3], 1 => 4]] )->collapse( 1 );
832
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => Map::from( [0 => ['b' => 2, 0 => 3], 1 => 4] )] )->collapse();
833
         *
834
         * Results:
835
         *  ['a' => 0, 'b' => 1, 'c' => 2, 'd' => 3]
836
         *  ['a' => 2, 'b' => 1]
837
         *  [0 => 3, 1 => 4, 'a' => 2]
838
         *  [0 => ['b' => 2, 0 => 3], 1 => 4, 'a' => 1]
839
         *  [0 => 3, 'a' => 1, 'b' => 2, 1 => 4]
840
         *
841
         * The keys are preserved and already existing elements will be overwritten.
842
         * This is also true for numeric keys! A value smaller than 1 for depth will
843
         * return the same map elements. Collapsing does also work if elements
844
         * implement the "Traversable" interface (which the Map object does).
845
         *
846
         * This method is similar than flat() but replaces already existing elements.
847
         *
848
         * @param int|null $depth Number of levels to collapse for multi-dimensional arrays or NULL for all
849
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
850
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
851
         */
852
        public function collapse( int $depth = null ) : self
853
        {
854
                if( $depth < 0 ) {
42✔
855
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
7✔
856
                }
857

858
                $result = [];
35✔
859
                $this->kflatten( $this->list(), $result, $depth ?? 0x7fffffff );
35✔
860
                return new static( $result );
35✔
861
        }
862

863

864
        /**
865
         * Combines the values of the map as keys with the passed elements as values.
866
         *
867
         * Examples:
868
         *  Map::from( ['name', 'age'] )->combine( ['Tom', 29] );
869
         *
870
         * Results:
871
         *  ['name' => 'Tom', 'age' => 29]
872
         *
873
         * @param iterable<int|string,mixed> $values Values of the new map
874
         * @return self<int|string,mixed> New map
875
         */
876
        public function combine( iterable $values ) : self
877
        {
878
                return new static( array_combine( $this->list(), $this->array( $values ) ) );
7✔
879
        }
880

881

882
        /**
883
         * Compares the value against all map elements.
884
         *
885
         * Examples:
886
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
887
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
888
         *  Map::from( [123, 12.3] )->compare( '12.3' );
889
         *  Map::from( [false, true] )->compare( '1' );
890
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
891
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
892
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
893
         *
894
         * Results:
895
         * The first four examples return TRUE, the last three examples will return FALSE.
896
         *
897
         * All scalar values (bool, float, int and string) are casted to string values before
898
         * comparing to the given value. Non-scalar values in the map are ignored.
899
         *
900
         * @param string $value Value to compare map elements to
901
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
902
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
903
         */
904
        public function compare( string $value, bool $case = true ) : bool
905
        {
906
                $fcn = $case ? 'strcmp' : 'strcasecmp';
7✔
907

908
                foreach( $this->list() as $item )
7✔
909
                {
910
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
7✔
911
                                return true;
7✔
912
                        }
913
                }
914

915
                return false;
7✔
916
        }
917

918

919
        /**
920
         * Pushs all of the given elements onto the map with new keys without creating a new map.
921
         *
922
         * Examples:
923
         *  Map::from( ['foo'] )->concat( new Map( ['bar'] ));
924
         *
925
         * Results:
926
         *  ['foo', 'bar']
927
         *
928
         * The keys of the passed elements are NOT preserved!
929
         *
930
         * @param iterable<int|string,mixed> $elements List of elements
931
         * @return self<int|string,mixed> Updated map for fluid interface
932
         */
933
        public function concat( iterable $elements ) : self
934
        {
935
                $this->list();
14✔
936

937
                foreach( $elements as $item ) {
14✔
938
                        $this->list[] = $item;
14✔
939
                }
940

941
                return $this;
14✔
942
        }
943

944

945
        /**
946
         * Determines if an item exists in the map.
947
         *
948
         * This method combines the power of the where() method with some() to check
949
         * if the map contains at least one of the passed values or conditions.
950
         *
951
         * Examples:
952
         *  Map::from( ['a', 'b'] )->contains( 'a' );
953
         *  Map::from( ['a', 'b'] )->contains( ['a', 'c'] );
954
         *  Map::from( ['a', 'b'] )->contains( function( $item, $key ) {
955
         *    return $item === 'a'
956
         *  } );
957
         *  Map::from( [['type' => 'name']] )->contains( 'type', 'name' );
958
         *  Map::from( [['type' => 'name']] )->contains( 'type', '==', 'name' );
959
         *
960
         * Results:
961
         * All method calls will return TRUE because at least "a" is included in the
962
         * map or there's a "type" key with a value "name" like in the last two
963
         * examples.
964
         *
965
         * Check the where() method for available operators.
966
         *
967
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
968
         * @param string|null $op Operator used for comparison
969
         * @param mixed $value Value used for comparison
970
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
971
         */
972
        public function contains( $key, string $operator = null, $value = null ) : bool
973
        {
974
                if( $operator === null ) {
14✔
975
                        return $this->some( $key );
7✔
976
                }
977

978
                if( $value === null ) {
7✔
979
                        return !$this->where( $key, '==', $operator )->isEmpty();
7✔
980
                }
981

982
                return !$this->where( $key, $operator, $value )->isEmpty();
7✔
983
        }
984

985

986
        /**
987
         * Creates a new map with the same elements.
988
         *
989
         * Both maps share the same array until one of the map objects modifies the
990
         * array. Then, the array is copied and the copy is modfied (copy on write).
991
         *
992
         * @return self<int|string,mixed> New map
993
         */
994
        public function copy() : self
995
        {
996
                return clone $this;
35✔
997
        }
998

999

1000
        /**
1001
         * Counts the total number of elements in the map.
1002
         *
1003
         * @return int Number of elements
1004
         */
1005
        public function count() : int
1006
        {
1007
                return count( $this->list() );
63✔
1008
        }
1009

1010

1011
        /**
1012
         * Counts how often the same values are in the map.
1013
         *
1014
         * Examples:
1015
         *  Map::from( [1, 'foo', 2, 'foo', 1] )->countBy();
1016
         *  Map::from( [1.11, 3.33, 3.33, 9.99] )->countBy();
1017
         *  Map::from( ['a@gmail.com', 'b@yahoo.com', 'c@gmail.com'] )->countBy( function( $email ) {
1018
         *    return substr( strrchr( $email, '@' ), 1 );
1019
         *  } );
1020
         *
1021
         * Results:
1022
         *  [1 => 2, 'foo' => 2, 2 => 1]
1023
         *  ['1.11' => 1, '3.33' => 2, '9.99' => 1]
1024
         *  ['gmail.com' => 2, 'yahoo.com' => 1]
1025
         *
1026
         * Counting values does only work for integers and strings because these are
1027
         * the only types allowed as array keys. All elements are casted to strings
1028
         * if no callback is passed. Custom callbacks need to make sure that only
1029
         * string or integer values are returned!
1030
         *
1031
         * @param  callable|null $callback Function with (value, key) parameters which returns the value to use for counting
1032
         * @return self<int|string,mixed> New map with values as keys and their count as value
1033
         */
1034
        public function countBy( callable $callback = null ) : self
1035
        {
1036
                $callback = $callback ?: function( $value ) {
15✔
1037
                        return (string) $value;
14✔
1038
                };
21✔
1039

1040
                return new static( array_count_values( array_map( $callback, $this->list() ) ) );
21✔
1041
        }
1042

1043

1044
        /**
1045
         * Dumps the map content and terminates the script.
1046
         *
1047
         * The dd() method is very helpful to see what are the map elements passed
1048
         * between two map methods in a method call chain. It stops execution of the
1049
         * script afterwards to avoid further output.
1050
         *
1051
         * Examples:
1052
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->sort()->dd();
1053
         *
1054
         * Results:
1055
         *  Array
1056
         *  (
1057
         *      [0] => bar
1058
         *      [1] => foo
1059
         *  )
1060
         *
1061
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1062
         */
1063
        public function dd( callable $callback = null ) : void
1064
        {
1065
                $this->dump( $callback );
×
1066
                exit( 1 );
×
1067
        }
1068

1069

1070
        /**
1071
         * Returns the keys/values in the map whose values are not present in the passed elements in a new map.
1072
         *
1073
         * Examples:
1074
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diff( ['bar'] );
1075
         *
1076
         * Results:
1077
         *  ['a' => 'foo']
1078
         *
1079
         * If a callback is passed, the given function will be used to compare the values.
1080
         * The function must accept two parameters (value A and B) and must return
1081
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
1082
         * greater than value B. Both, a method name and an anonymous function can be passed:
1083
         *
1084
         *  Map::from( [0 => 'a'] )->diff( [0 => 'A'], 'strcasecmp' );
1085
         *  Map::from( ['b' => 'a'] )->diff( ['B' => 'A'], 'strcasecmp' );
1086
         *  Map::from( ['b' => 'a'] )->diff( ['c' => 'A'], function( $valA, $valB ) {
1087
         *      return strtolower( $valA ) <=> strtolower( $valB );
1088
         *  } );
1089
         *
1090
         * All examples will return an empty map because both contain the same values
1091
         * when compared case insensitive.
1092
         *
1093
         * The keys are preserved using this method.
1094
         *
1095
         * @param iterable<int|string,mixed> $elements List of elements
1096
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
1097
         * @return self<int|string,mixed> New map
1098
         */
1099
        public function diff( iterable $elements, callable $callback = null ) : self
1100
        {
1101
                if( $callback ) {
21✔
1102
                        return new static( array_udiff( $this->list(), $this->array( $elements ), $callback ) );
7✔
1103
                }
1104

1105
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
21✔
1106
        }
1107

1108

1109
        /**
1110
         * Returns the keys/values in the map whose keys AND values are not present in the passed elements in a new map.
1111
         *
1112
         * Examples:
1113
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diffAssoc( new Map( ['foo', 'b' => 'bar'] ) );
1114
         *
1115
         * Results:
1116
         *  ['a' => 'foo']
1117
         *
1118
         * If a callback is passed, the given function will be used to compare the values.
1119
         * The function must accept two parameters (value A and B) and must return
1120
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
1121
         * greater than value B. Both, a method name and an anonymous function can be passed:
1122
         *
1123
         *  Map::from( [0 => 'a'] )->diffAssoc( [0 => 'A'], 'strcasecmp' );
1124
         *  Map::from( ['b' => 'a'] )->diffAssoc( ['B' => 'A'], 'strcasecmp' );
1125
         *  Map::from( ['b' => 'a'] )->diffAssoc( ['c' => 'A'], function( $valA, $valB ) {
1126
         *      return strtolower( $valA ) <=> strtolower( $valB );
1127
         *  } );
1128
         *
1129
         * The first example will return an empty map because both contain the same
1130
         * values when compared case insensitive. The second and third example will return
1131
         * an empty map because 'A' is part of the passed array but the keys doesn't match
1132
         * ("b" vs. "B" and "b" vs. "c").
1133
         *
1134
         * The keys are preserved using this method.
1135
         *
1136
         * @param iterable<int|string,mixed> $elements List of elements
1137
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
1138
         * @return self<int|string,mixed> New map
1139
         */
1140
        public function diffAssoc( iterable $elements, callable $callback = null ) : self
1141
        {
1142
                if( $callback ) {
14✔
1143
                        return new static( array_diff_uassoc( $this->list(), $this->array( $elements ), $callback ) );
7✔
1144
                }
1145

1146
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
14✔
1147
        }
1148

1149

1150
        /**
1151
         * Returns the key/value pairs from the map whose keys are not present in the passed elements in a new map.
1152
         *
1153
         * Examples:
1154
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diffKeys( new Map( ['foo', 'b' => 'baz'] ) );
1155
         *
1156
         * Results:
1157
         *  ['a' => 'foo']
1158
         *
1159
         * If a callback is passed, the given function will be used to compare the keys.
1160
         * The function must accept two parameters (key A and B) and must return
1161
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
1162
         * greater than key B. Both, a method name and an anonymous function can be passed:
1163
         *
1164
         *  Map::from( [0 => 'a'] )->diffKeys( [0 => 'A'], 'strcasecmp' );
1165
         *  Map::from( ['b' => 'a'] )->diffKeys( ['B' => 'X'], 'strcasecmp' );
1166
         *  Map::from( ['b' => 'a'] )->diffKeys( ['c' => 'a'], function( $keyA, $keyB ) {
1167
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
1168
         *  } );
1169
         *
1170
         * The first and second example will return an empty map because both contain
1171
         * the same keys when compared case insensitive. The third example will return
1172
         * ['b' => 'a'] because the keys doesn't match ("b" vs. "c").
1173
         *
1174
         * The keys are preserved using this method.
1175
         *
1176
         * @param iterable<int|string,mixed> $elements List of elements
1177
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
1178
         * @return self<int|string,mixed> New map
1179
         */
1180
        public function diffKeys( iterable $elements, callable $callback = null ) : self
1181
        {
1182
                if( $callback ) {
14✔
1183
                        return new static( array_diff_ukey( $this->list(), $this->array( $elements ), $callback ) );
7✔
1184
                }
1185

1186
                return new static( array_diff_key( $this->list(), $this->array( $elements ) ) );
14✔
1187
        }
1188

1189

1190
        /**
1191
         * Dumps the map content using the given function (print_r by default).
1192
         *
1193
         * The dump() method is very helpful to see what are the map elements passed
1194
         * between two map methods in a method call chain.
1195
         *
1196
         * Examples:
1197
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->dump()->asort()->dump( 'var_dump' );
1198
         *
1199
         * Results:
1200
         *  Array
1201
         *  (
1202
         *      [a] => foo
1203
         *      [b] => bar
1204
         *  )
1205
         *  array(1) {
1206
         *    ["b"]=>
1207
         *    string(3) "bar"
1208
         *    ["a"]=>
1209
         *    string(3) "foo"
1210
         *  }
1211
         *
1212
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1213
         * @return self<int|string,mixed> Same map for fluid interface
1214
         */
1215
        public function dump( callable $callback = null ) : self
1216
        {
1217
                $callback ? $callback( $this->list() ) : print_r( $this->list() );
7✔
1218
                return $this;
7✔
1219
        }
1220

1221

1222
        /**
1223
         * Returns the duplicate values from the map.
1224
         *
1225
         * For nested arrays, you have to pass the name of the column of the nested
1226
         * array which should be used to check for duplicates.
1227
         *
1228
         * Examples:
1229
         *  Map::from( [1, 2, '1', 3] )->duplicates()
1230
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->duplicates( 'p' )
1231
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( 'i/p' )
1232
         *
1233
         * Results:
1234
         *  [2 => '1']
1235
         *  [1 => ['p' => 1]]
1236
         *  [1 => ['i' => ['p' => '1']]]
1237
         *
1238
         * This does also work for multi-dimensional arrays by passing the keys
1239
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1240
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1241
         * public properties of objects or objects implementing __isset() and __get() methods.
1242
         *
1243
         * The keys are preserved using this method.
1244
         *
1245
         * @param string|null $key Key or path of the nested array or object to check for
1246
         * @return self<int|string,mixed> New map
1247
         */
1248
        public function duplicates( string $key = null ) : self
1249
        {
1250
                $list = $this->list();
21✔
1251
                $items = ( $key !== null ? $this->col( $key )->toArray() : $list );
21✔
1252

1253
                return new static( array_diff_key( $list, array_unique( $items ) ) );
21✔
1254
        }
1255

1256

1257
        /**
1258
         * Executes a callback over each entry until FALSE is returned.
1259
         *
1260
         * Examples:
1261
         *  $result = [];
1262
         *  Map::from( [0 => 'a', 1 => 'b'] )->each( function( $value, $key ) use ( &$result ) {
1263
         *      $result[$key] = strtoupper( $value );
1264
         *      return false;
1265
         *  } );
1266
         *
1267
         * The $result array will contain [0 => 'A'] because FALSE is returned
1268
         * after the first entry and all other entries are then skipped.
1269
         *
1270
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1271
         * @return self<int|string,mixed> Same map for fluid interface
1272
         */
1273
        public function each( \Closure $callback ) : self
1274
        {
1275
                foreach( $this->list() as $key => $item )
14✔
1276
                {
1277
                        if( $callback( $item, $key ) === false ) {
14✔
1278
                                break;
8✔
1279
                        }
1280
                }
1281

1282
                return $this;
14✔
1283
        }
1284

1285

1286
        /**
1287
         * Determines if the map is empty or not.
1288
         *
1289
         * Examples:
1290
         *  Map::from( [] )->empty();
1291
         *  Map::from( ['a'] )->empty();
1292
         *
1293
         * Results:
1294
         *  The first example returns TRUE while the second returns FALSE
1295
         *
1296
         * The method is equivalent to isEmpty().
1297
         *
1298
         * @return bool TRUE if map is empty, FALSE if not
1299
         */
1300
        public function empty() : bool
1301
        {
1302
                return empty( $this->list() );
14✔
1303
        }
1304

1305

1306
        /**
1307
         * Tests if the passed elements are equal to the elements in the map.
1308
         *
1309
         * Examples:
1310
         *  Map::from( ['a'] )->equals( ['a', 'b'] );
1311
         *  Map::from( ['a', 'b'] )->equals( ['b'] );
1312
         *  Map::from( ['a', 'b'] )->equals( ['b', 'a'] );
1313
         *
1314
         * Results:
1315
         * The first and second example will return FALSE, the third example will return TRUE
1316
         *
1317
         * The method differs to is() in the fact that it doesn't care about the keys
1318
         * by default. The elements are only loosely compared and the keys are ignored.
1319
         *
1320
         * Values are compared by their string values:
1321
         * (string) $item1 === (string) $item2
1322
         *
1323
         * @param iterable<int|string,mixed> $elements List of elements to test against
1324
         * @return bool TRUE if both are equal, FALSE if not
1325
         */
1326
        public function equals( iterable $elements ) : bool
1327
        {
1328
                $list = $this->list();
42✔
1329
                $elements = $this->array( $elements );
42✔
1330

1331
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
42✔
1332
        }
1333

1334

1335
        /**
1336
         * Verifies that all elements pass the test of the given callback.
1337
         *
1338
         * Examples:
1339
         *  Map::from( [0 => 'a', 1 => 'b'] )->every( function( $value, $key ) {
1340
         *      return is_string( $value );
1341
         *  } );
1342
         *
1343
         *  Map::from( [0 => 'a', 1 => 100] )->every( function( $value, $key ) {
1344
         *      return is_string( $value );
1345
         *  } );
1346
         *
1347
         * The first example will return TRUE because all values are a string while
1348
         * the second example will return FALSE.
1349
         *
1350
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1351
         * @return bool True if all elements pass the test, false if if fails for at least one element
1352
         */
1353
        public function every( \Closure $callback ) : bool
1354
        {
1355
                foreach( $this->list() as $key => $item )
7✔
1356
                {
1357
                        if( $callback( $item, $key ) === false ) {
7✔
1358
                                return false;
7✔
1359
                        }
1360
                }
1361

1362
                return true;
7✔
1363
        }
1364

1365

1366
        /**
1367
         * Returns a new map without the passed element keys.
1368
         *
1369
         * Examples:
1370
         *  Map::from( ['a' => 1, 'b' => 2, 'c' => 3] )->except( 'b' );
1371
         *  Map::from( [1 => 'a', 2 => 'b', 3 => 'c'] )->except( [1, 3] );
1372
         *
1373
         * Results:
1374
         *  ['a' => 1, 'c' => 3]
1375
         *  [2 => 'b']
1376
         *
1377
         * The keys in the result map are preserved.
1378
         *
1379
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
1380
         * @return self<int|string,mixed> New map
1381
         */
1382
        public function except( $keys ) : self
1383
        {
1384
                return $this->copy()->remove( $keys );
7✔
1385
        }
1386

1387

1388
        /**
1389
         * Applies a filter to all elements of the map and returns a new map.
1390
         *
1391
         * Examples:
1392
         *  Map::from( [null, 0, 1, '', '0', 'a'] )->filter();
1393
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->filter( function( $value, $key ) {
1394
         *      return $key < 10 && $value < 'n';
1395
         *  } );
1396
         *
1397
         * Results:
1398
         *  [1, 'a']
1399
         *  ['a', 'b']
1400
         *
1401
         * If no callback is passed, all values which are empty, null or false will be
1402
         * removed if their value converted to boolean is FALSE:
1403
         *  (bool) $value === false
1404
         *
1405
         * The keys in the result map are preserved.
1406
         *
1407
         * @param  callable|null $callback Function with (item, key) parameters and returns TRUE/FALSE
1408
         * @return self<int|string,mixed> New map
1409
         */
1410
        public function filter( callable $callback = null ) : self
1411
        {
1412
                if( $callback ) {
70✔
1413
                        return new static( array_filter( $this->list(), $callback, ARRAY_FILTER_USE_BOTH ) );
63✔
1414
                }
1415

1416
                return new static( array_filter( $this->list() ) );
7✔
1417
        }
1418

1419

1420
        /**
1421
         * Returns the first/last matching element where the callback returns TRUE.
1422
         *
1423
         * Examples:
1424
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1425
         *      return $value >= 'b';
1426
         *  } );
1427
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1428
         *      return $value >= 'b';
1429
         *  }, null, true );
1430
         *  Map::from( [] )->find( function( $value, $key ) {
1431
         *      return $value >= 'b';
1432
         *  }, 'none' );
1433
         *  Map::from( [] )->find( function( $value, $key ) {
1434
         *      return $value >= 'b';
1435
         *  }, new \Exception( 'error' ) );
1436
         *
1437
         * Results:
1438
         * The first example will return 'c' while the second will return 'e' (last element).
1439
         * The third one will return "none" and the last one will throw the exception.
1440
         *
1441
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1442
         * @param mixed $default Default value or exception if the map contains no elements
1443
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1444
         * @return mixed First matching value, passed default value or an exception
1445
         */
1446
        public function find( \Closure $callback, $default = null, bool $reverse = false )
1447
        {
1448
                foreach( ( $reverse ? array_reverse( $this->list() ) : $this->list() ) as $key => $value )
28✔
1449
                {
1450
                        if( $callback( $value, $key ) ) {
28✔
1451
                                return $value;
16✔
1452
                        }
1453
                }
1454

1455
                if( $default instanceof \Throwable ) {
14✔
1456
                        throw $default;
7✔
1457
                }
1458

1459
                return $default;
7✔
1460
        }
1461

1462

1463
        /**
1464
         * Returns the first element from the map.
1465
         *
1466
         * Examples:
1467
         *  Map::from( ['a', 'b'] )->first();
1468
         *  Map::from( [] )->first( 'x' );
1469
         *  Map::from( [] )->first( new \Exception( 'error' ) );
1470
         *  Map::from( [] )->first( function() { return rand(); } );
1471
         *
1472
         * Results:
1473
         * The first example will return 'b' and the second one 'x'. The third example
1474
         * will throw the exception passed if the map contains no elements. In the
1475
         * fourth example, a random value generated by the closure function will be
1476
         * returned.
1477
         *
1478
         * @param mixed $default Default value or exception if the map contains no elements
1479
         * @return mixed First value of map, (generated) default value or an exception
1480
         */
1481
        public function first( $default = null )
1482
        {
1483
                if( ( $value = reset( $this->list() ) ) !== false ) {
56✔
1484
                        return $value;
35✔
1485
                }
1486

1487
                if( $default instanceof \Closure ) {
28✔
1488
                        return $default();
7✔
1489
                }
1490

1491
                if( $default instanceof \Throwable ) {
21✔
1492
                        throw $default;
7✔
1493
                }
1494

1495
                return $default;
14✔
1496
        }
1497

1498

1499
        /**
1500
         * Returns the first key from the map.
1501
         *
1502
         * Examples:
1503
         *  Map::from( ['a' => 1, 'b' => 2] )->firstKey();
1504
         *  Map::from( [] )->firstKey();
1505
         *
1506
         * Results:
1507
         * The first example will return 'a' and the second one NULL.
1508
         *
1509
         * @return mixed First key of map or NULL if empty
1510
         */
1511
        public function firstKey()
1512
        {
1513
                $list = $this->list();
14✔
1514

1515
                if( function_exists( 'array_key_first' ) ) {
14✔
1516
                        return array_key_first( $list );
14✔
1517
                }
1518

1519
                reset( $list );
×
1520
                return key( $list );
×
1521
        }
1522

1523

1524
        /**
1525
         * Creates a new map with all sub-array elements added recursively withput overwriting existing keys.
1526
         *
1527
         * Examples:
1528
         *  Map::from( [[0, 1], [2, 3]] )->flat();
1529
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat();
1530
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat( 1 );
1531
         *  Map::from( [[0, 1], Map::from( [[2, 3], 4] )] )->flat();
1532
         *
1533
         * Results:
1534
         *  [0, 1, 2, 3]
1535
         *  [0, 1, 2, 3, 4]
1536
         *  [0, 1, [2, 3], 4]
1537
         *  [0, 1, 2, 3, 4]
1538
         *
1539
         * The keys are not preserved and the new map elements will be numbered from
1540
         * 0-n. A value smaller than 1 for depth will return the same map elements
1541
         * indexed from 0-n. Flattening does also work if elements implement the
1542
         * "Traversable" interface (which the Map object does).
1543
         *
1544
         * This method is similar than collapse() but doesn't replace existing elements.
1545
         * Keys are NOT preserved using this method!
1546
         *
1547
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1548
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
1549
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1550
         */
1551
        public function flat( int $depth = null ) : self
1552
        {
1553
                if( $depth < 0 ) {
42✔
1554
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
7✔
1555
                }
1556

1557
                $result = [];
35✔
1558
                $this->flatten( $this->list(), $result, $depth ?? 0x7fffffff );
35✔
1559
                return new static( $result );
35✔
1560
        }
1561

1562

1563
        /**
1564
         * Exchanges the keys with their values and vice versa.
1565
         *
1566
         * Examples:
1567
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->flip();
1568
         *
1569
         * Results:
1570
         *  ['X' => 'a', 'Y' => 'b']
1571
         *
1572
         * @return self<int|string,mixed> New map with keys as values and values as keys
1573
         */
1574
        public function flip() : self
1575
        {
1576
                return new static( array_flip( $this->list() ) );
7✔
1577
        }
1578

1579

1580
        /**
1581
         * Returns an element by key and casts it to float if possible.
1582
         *
1583
         * Examples:
1584
         *  Map::from( ['a' => true] )->float( 'a' );
1585
         *  Map::from( ['a' => 1] )->float( 'a' );
1586
         *  Map::from( ['a' => '1.1'] )->float( 'a' );
1587
         *  Map::from( ['a' => '10'] )->float( 'a' );
1588
         *  Map::from( ['a' => ['b' => ['c' => 1.1]]] )->float( 'a/b/c' );
1589
         *  Map::from( [] )->float( 'c', function() { return 1.1; } );
1590
         *  Map::from( [] )->float( 'a', 1.1 );
1591
         *
1592
         *  Map::from( [] )->float( 'b' );
1593
         *  Map::from( ['b' => ''] )->float( 'b' );
1594
         *  Map::from( ['b' => null] )->float( 'b' );
1595
         *  Map::from( ['b' => 'abc'] )->float( 'b' );
1596
         *  Map::from( ['b' => [1]] )->float( 'b' );
1597
         *  Map::from( ['b' => #resource] )->float( 'b' );
1598
         *  Map::from( ['b' => new \stdClass] )->float( 'b' );
1599
         *
1600
         *  Map::from( [] )->float( 'c', new \Exception( 'error' ) );
1601
         *
1602
         * Results:
1603
         * The first eight examples will return the float values for the passed keys
1604
         * while the 9th to 14th example returns 0. The last example will throw an exception.
1605
         *
1606
         * This does also work for multi-dimensional arrays by passing the keys
1607
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1608
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1609
         * public properties of objects or objects implementing __isset() and __get() methods.
1610
         *
1611
         * @param int|string $key Key or path to the requested item
1612
         * @param mixed $default Default value if key isn't found (will be casted to float)
1613
         * @return float Value from map or default value
1614
         */
1615
        public function float( $key, $default = 0.0 ) : float
1616
        {
1617
                return (float) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
21✔
1618
        }
1619

1620

1621
        /**
1622
         * Returns an element from the map by key.
1623
         *
1624
         * Examples:
1625
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'a' );
1626
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'c', 'Z' );
1627
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->get( 'a/b/c' );
1628
         *  Map::from( [] )->get( 'Y', new \Exception( 'error' ) );
1629
         *  Map::from( [] )->get( 'Y', function() { return rand(); } );
1630
         *
1631
         * Results:
1632
         * The first example will return 'X', the second 'Z' and the third 'Y'. The forth
1633
         * example will throw the exception passed if the map contains no elements. In
1634
         * the fifth example, a random value generated by the closure function will be
1635
         * returned.
1636
         *
1637
         * This does also work for multi-dimensional arrays by passing the keys
1638
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1639
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1640
         * public properties of objects or objects implementing __isset() and __get() methods.
1641
         *
1642
         * @param int|string $key Key or path to the requested item
1643
         * @param mixed $default Default value if no element matches
1644
         * @return mixed Value from map or default value
1645
         */
1646
        public function get( $key, $default = null )
1647
        {
1648
                $list = $this->list();
161✔
1649

1650
                if( array_key_exists( $key, $list ) ) {
161✔
1651
                        return $list[$key];
42✔
1652
                }
1653

1654
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
147✔
1655
                        return $v;
49✔
1656
                }
1657

1658
                if( $default instanceof \Closure ) {
126✔
1659
                        return $default();
42✔
1660
                }
1661

1662
                if( $default instanceof \Throwable ) {
84✔
1663
                        throw $default;
42✔
1664
                }
1665

1666
                return $default;
42✔
1667
        }
1668

1669

1670
        /**
1671
         * Returns an iterator for the elements.
1672
         *
1673
         * This method will be used by e.g. foreach() to loop over all entries:
1674
         *  foreach( Map::from( ['a', 'b'] ) as $value )
1675
         *
1676
         * @return \ArrayIterator<int|string,mixed> Iterator for map elements
1677
         */
1678
        public function getIterator() : \ArrayIterator
1679
        {
1680
                return new \ArrayIterator( $this->list = $this->array( $this->list ) );
35✔
1681
        }
1682

1683

1684
        /**
1685
         * Returns only items which matches the regular expression.
1686
         *
1687
         * All items are converted to string first before they are compared to the
1688
         * regular expression. Thus, fractions of ".0" will be removed in float numbers
1689
         * which may result in unexpected results.
1690
         *
1691
         * Examples:
1692
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/b/' );
1693
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/a/', PREG_GREP_INVERT );
1694
         *  Map::from( [1.5, 0, 1.0, 'a'] )->grep( '/^(\d+)?\.\d+$/' );
1695
         *
1696
         * Results:
1697
         *  ['ab', 'bc']
1698
         *  ['bc', 'cd']
1699
         *  [1.5] // float 1.0 is converted to string "1"
1700
         *
1701
         * The keys are preserved using this method.
1702
         *
1703
         * @param string $pattern Regular expression pattern, e.g. "/ab/"
1704
         * @param int $flags PREG_GREP_INVERT to return elements not matching the pattern
1705
         * @return self<int|string,mixed> New map containing only the matched elements
1706
         */
1707
        public function grep( string $pattern, int $flags = 0 ) : self
1708
        {
1709
                if( ( $result = preg_grep( $pattern, $this->list(), $flags ) ) === false )
35✔
1710
                {
1711
                        switch( preg_last_error() )
7✔
1712
                        {
1713
                                case PREG_INTERNAL_ERROR: $msg = 'Internal error'; break;
7✔
1714
                                case PREG_BACKTRACK_LIMIT_ERROR: $msg = 'Backtrack limit error'; break;
×
1715
                                case PREG_RECURSION_LIMIT_ERROR: $msg = 'Recursion limit error'; break;
×
1716
                                case PREG_BAD_UTF8_ERROR: $msg = 'Bad UTF8 error'; break;
×
1717
                                case PREG_BAD_UTF8_OFFSET_ERROR: $msg = 'Bad UTF8 offset error'; break;
×
1718
                                case PREG_JIT_STACKLIMIT_ERROR: $msg = 'JIT stack limit error'; break;
×
1719
                                default: $msg = 'Unknown error';
×
1720
                        }
1721

1722
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
7✔
1723
                }
1724

1725
                return new static( $result );
21✔
1726
        }
1727

1728

1729
        /**
1730
         * Groups associative array elements or objects by the passed key or closure.
1731
         *
1732
         * Instead of overwriting items with the same keys like to the col() method
1733
         * does, groupBy() keeps all entries in sub-arrays. It's preserves the keys
1734
         * of the orignal map entries too.
1735
         *
1736
         * Examples:
1737
         *  $list = [
1738
         *    10 => ['aid' => 123, 'code' => 'x-abc'],
1739
         *    20 => ['aid' => 123, 'code' => 'x-def'],
1740
         *    30 => ['aid' => 456, 'code' => 'x-def']
1741
         *  ];
1742
         *  Map::from( $list )->groupBy( 'aid' );
1743
         *  Map::from( $list )->groupBy( function( $item, $key ) {
1744
         *    return substr( $item['code'], -3 );
1745
         *  } );
1746
         *  Map::from( $list )->groupBy( 'xid' );
1747
         *
1748
         * Results:
1749
         *  [
1750
         *    123 => [10 => ['aid' => 123, 'code' => 'x-abc'], 20 => ['aid' => 123, 'code' => 'x-def']],
1751
         *    456 => [30 => ['aid' => 456, 'code' => 'x-def']]
1752
         *  ]
1753
         *  [
1754
         *    'abc' => [10 => ['aid' => 123, 'code' => 'x-abc']],
1755
         *    'def' => [20 => ['aid' => 123, 'code' => 'x-def'], 30 => ['aid' => 456, 'code' => 'x-def']]
1756
         *  ]
1757
         *  [
1758
         *    '' => [
1759
         *      10 => ['aid' => 123, 'code' => 'x-abc'],
1760
         *      20 => ['aid' => 123, 'code' => 'x-def'],
1761
         *      30 => ['aid' => 456, 'code' => 'x-def']
1762
         *    ]
1763
         *  ]
1764
         *
1765
         * In case the passed key doesn't exist in one or more items, these items
1766
         * are stored in a sub-array using an empty string as key.
1767
         *
1768
         * @param  \Closure|string|int $key Closure function with (item, idx) parameters returning the key or the key itself to group by
1769
         * @return self<int|string,mixed> New map with elements grouped by the given key
1770
         */
1771
        public function groupBy( $key ) : self
1772
        {
1773
                $result = [];
28✔
1774

1775
                foreach( $this->list() as $idx => $item )
28✔
1776
                {
1777
                        if( is_callable( $key ) ) {
28✔
1778
                                $keyval = $key( $item, $idx );
7✔
1779
                        } elseif( ( is_array( $item ) || $item instanceof \ArrayAccess ) && isset( $item[$key] ) ) {
21✔
1780
                                $keyval = $item[$key];
7✔
1781
                        } elseif( is_object( $item ) && isset( $item->{$key} ) ) {
14✔
1782
                                $keyval = $item->{$key};
7✔
1783
                        } else {
1784
                                $keyval = '';
7✔
1785
                        }
1786

1787
                        $result[$keyval][$idx] = $item;
28✔
1788
                }
1789

1790
                return new static( $result );
28✔
1791
        }
1792

1793

1794
        /**
1795
         * Determines if a key or several keys exists in the map.
1796
         *
1797
         * If several keys are passed as array, all keys must exist in the map for
1798
         * TRUE to be returned.
1799
         *
1800
         * Examples:
1801
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'a' );
1802
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'b'] );
1803
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->has( 'a/b/c' );
1804
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'c' );
1805
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'c'] );
1806
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'X' );
1807
         *
1808
         * Results:
1809
         * The first three examples will return TRUE while the other ones will return FALSE
1810
         *
1811
         * This does also work for multi-dimensional arrays by passing the keys
1812
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1813
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1814
         * public properties of objects or objects implementing __isset() and __get() methods.
1815
         *
1816
         * @param array<int|string>|int|string $key Key of the requested item or list of keys
1817
         * @return bool TRUE if key or keys are available in map, FALSE if not
1818
         */
1819
        public function has( $key ) : bool
1820
        {
1821
                $list = $this->list();
21✔
1822

1823
                foreach( (array) $key as $entry )
21✔
1824
                {
1825
                        if( array_key_exists( $entry, $list ) === false
21✔
1826
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
21✔
1827
                        ) {
1828
                                return false;
21✔
1829
                        }
1830
                }
1831

1832
                return true;
21✔
1833
        }
1834

1835

1836
        /**
1837
         * Executes callbacks depending on the condition.
1838
         *
1839
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
1840
         * executed and their returned value is passed back within a Map object. In
1841
         * case no "then" or "else" closure is given, the method will return the same
1842
         * map object.
1843
         *
1844
         * Examples:
1845
         *  Map::from( [] )->if( strpos( 'abc', 'b' ) !== false, function( $map ) {
1846
         *    echo 'found';
1847
         *  } );
1848
         *
1849
         *  Map::from( [] )->if( function( $map ) {
1850
         *    return $map->empty();
1851
         *  }, function( $map ) {
1852
         *    echo 'then';
1853
         *  } );
1854
         *
1855
         *  Map::from( ['a'] )->if( function( $map ) {
1856
         *    return $map->empty();
1857
         *  }, function( $map ) {
1858
         *    echo 'then';
1859
         *  }, function( $map ) {
1860
         *    echo 'else';
1861
         *  } );
1862
         *
1863
         *  Map::from( ['a', 'b'] )->if( true, function( $map ) {
1864
         *    return $map->push( 'c' );
1865
         *  } );
1866
         *
1867
         *  Map::from( ['a', 'b'] )->if( false, null, function( $map ) {
1868
         *    return $map->pop();
1869
         *  } );
1870
         *
1871
         * Results:
1872
         * The first example returns "found" while the second one returns "then" and
1873
         * the third one "else". The forth one will return ['a', 'b', 'c'] while the
1874
         * fifth one will return 'b', which is turned into a map of ['b'] again.
1875
         *
1876
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
1877
         * (a short form for anonymous closures) as parameters. The automatically have access
1878
         * to previously defined variables but can not modify them. Also, they can not have
1879
         * a void return type and must/will always return something. Details about
1880
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
1881
         *
1882
         * @param \Closure|bool $condition Boolean or function with (map) parameter returning a boolean
1883
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
1884
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
1885
         * @return self<int|string,mixed> New map
1886
         */
1887
        public function if( $condition, \Closure $then = null, \Closure $else = null ) : self
1888
        {
1889
                if( $condition instanceof \Closure ) {
56✔
1890
                        $condition = $condition( $this );
14✔
1891
                }
1892

1893
                if( $condition ) {
56✔
1894
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
35✔
1895
                } elseif( $else ) {
21✔
1896
                        return static::from( $else( $this, $condition ) );
21✔
1897
                }
1898

1899
                return $this;
×
1900
        }
1901

1902

1903
        /**
1904
         * Executes callbacks depending if the map contains elements or not.
1905
         *
1906
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
1907
         * executed and their returned value is passed back within a Map object. In
1908
         * case no "then" or "else" closure is given, the method will return the same
1909
         * map object.
1910
         *
1911
         * Examples:
1912
         *  Map::from( ['a'] )->ifAny( function( $map ) {
1913
         *    $map->push( 'b' );
1914
         *  } );
1915
         *
1916
         *  Map::from( [] )->ifAny( null, function( $map ) {
1917
         *    return $map->push( 'b' );
1918
         *  } );
1919
         *
1920
         *  Map::from( ['a'] )->ifAny( function( $map ) {
1921
         *    return 'c';
1922
         *  } );
1923
         *
1924
         * Results:
1925
         * The first example returns a Map containing ['a', 'b'] because the the initial
1926
         * Map is not empty. The second one returns  a Map with ['b'] because the initial
1927
         * Map is empty and the "else" closure is used. The last example returns ['c']
1928
         * as new map content.
1929
         *
1930
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
1931
         * (a short form for anonymous closures) as parameters. The automatically have access
1932
         * to previously defined variables but can not modify them. Also, they can not have
1933
         * a void return type and must/will always return something. Details about
1934
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
1935
         *
1936
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
1937
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
1938
         * @return self<int|string,mixed> New map
1939
         */
1940
        public function ifAny( \Closure $then = null, \Closure $else = null ) : self
1941
        {
1942
                return $this->if( !empty( $this->list() ), $then, $else );
21✔
1943
        }
1944

1945

1946
        /**
1947
         * Executes callbacks depending if the map is empty or not.
1948
         *
1949
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
1950
         * executed and their returned value is passed back within a Map object. In
1951
         * case no "then" or "else" closure is given, the method will return the same
1952
         * map object.
1953
         *
1954
         * Examples:
1955
         *  Map::from( [] )->ifEmpty( function( $map ) {
1956
         *    $map->push( 'a' );
1957
         *  } );
1958
         *
1959
         *  Map::from( ['a'] )->ifEmpty( null, function( $map ) {
1960
         *    return $map->push( 'b' );
1961
         *  } );
1962
         *
1963
         * Results:
1964
         * The first example returns a Map containing ['a'] because the the initial Map
1965
         * is empty. The second one returns  a Map with ['a', 'b'] because the initial
1966
         * Map is not empty and the "else" closure is used.
1967
         *
1968
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
1969
         * (a short form for anonymous closures) as parameters. The automatically have access
1970
         * to previously defined variables but can not modify them. Also, they can not have
1971
         * a void return type and must/will always return something. Details about
1972
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
1973
         *
1974
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
1975
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
1976
         * @return self<int|string,mixed> New map
1977
         */
1978
        public function ifEmpty( \Closure $then = null, \Closure $else = null ) : self
1979
        {
1980
                return $this->if( empty( $this->list() ), $then, $else );
×
1981
        }
1982

1983

1984
        /**
1985
         * Tests if all entries in the map are objects implementing the given interface.
1986
         *
1987
         * Examples:
1988
         *  Map::from( [new Map(), new Map()] )->implements( '\Countable' );
1989
         *  Map::from( [new Map(), new stdClass()] )->implements( '\Countable' );
1990
         *  Map::from( [new Map(), 123] )->implements( '\Countable' );
1991
         *  Map::from( [new Map(), 123] )->implements( '\Countable', true );
1992
         *  Map::from( [new Map(), 123] )->implements( '\Countable', '\RuntimeException' );
1993
         *
1994
         * Results:
1995
         *  The first example returns TRUE while the second and third one return FALSE.
1996
         *  The forth example will throw an UnexpectedValueException while the last one
1997
         *  throws a RuntimeException.
1998
         *
1999
         * @param string $interface Name of the interface that must be implemented
2000
         * @param \Throwable|bool $throw Passing TRUE or an exception name will throw the exception instead of returning FALSE
2001
         * @return bool TRUE if all entries implement the interface or FALSE if at least one doesn't
2002
         * @throws \UnexpectedValueException|\Throwable If one entry doesn't implement the interface
2003
         */
2004
        public function implements( string $interface, $throw = false ) : bool
2005
        {
2006
                foreach( $this->list() as $entry )
21✔
2007
                {
2008
                        if( !( $entry instanceof $interface ) )
21✔
2009
                        {
2010
                                if( $throw )
21✔
2011
                                {
2012
                                        $name = is_string( $throw ) ? $throw : '\UnexpectedValueException';
14✔
2013
                                        throw new $name( "Does not implement $interface: " . print_r( $entry, true ) );
14✔
2014
                                }
2015

2016
                                return false;
9✔
2017
                        }
2018
                }
2019

2020
                return true;
7✔
2021
        }
2022

2023

2024
        /**
2025
         * Tests if the passed element or elements are part of the map.
2026
         *
2027
         * Examples:
2028
         *  Map::from( ['a', 'b'] )->in( 'a' );
2029
         *  Map::from( ['a', 'b'] )->in( ['a', 'b'] );
2030
         *  Map::from( ['a', 'b'] )->in( 'x' );
2031
         *  Map::from( ['a', 'b'] )->in( ['a', 'x'] );
2032
         *  Map::from( ['1', '2'] )->in( 2, true );
2033
         *
2034
         * Results:
2035
         * The first and second example will return TRUE while the other ones will return FALSE
2036
         *
2037
         * @param mixed|array $element Element or elements to search for in the map
2038
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2039
         * @return bool TRUE if all elements are available in map, FALSE if not
2040
         */
2041
        public function in( $element, bool $strict = false ) : bool
2042
        {
2043
                if( !is_array( $element ) ) {
28✔
2044
                        return in_array( $element, $this->list(), $strict );
28✔
2045
                };
2046

2047
                foreach( $element as $entry )
7✔
2048
                {
2049
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
7✔
2050
                                return false;
7✔
2051
                        }
2052
                }
2053

2054
                return true;
7✔
2055
        }
2056

2057

2058
        /**
2059
         * Tests if the passed element or elements are part of the map.
2060
         *
2061
         * Examples:
2062
         *  Map::from( ['a', 'b'] )->includes( 'a' );
2063
         *  Map::from( ['a', 'b'] )->includes( ['a', 'b'] );
2064
         *  Map::from( ['a', 'b'] )->includes( 'x' );
2065
         *  Map::from( ['a', 'b'] )->includes( ['a', 'x'] );
2066
         *  Map::from( ['1', '2'] )->includes( 2, true );
2067
         *
2068
         * Results:
2069
         * The first and second example will return TRUE while the other ones will return FALSE
2070
         *
2071
         * This method is an alias for in(). For performance reasons, in() should be
2072
         * preferred because it uses one method call less than includes().
2073
         *
2074
         * @param mixed|array $element Element or elements to search for in the map
2075
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2076
         * @return bool TRUE if all elements are available in map, FALSE if not
2077
         */
2078
        public function includes( $element, bool $strict = false ) : bool
2079
        {
2080
                return $this->in( $element, $strict );
7✔
2081
        }
2082

2083

2084
        /**
2085
         * Returns the numerical index of the given key.
2086
         *
2087
         * Examples:
2088
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( '8' );
2089
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( function( $key ) {
2090
         *      return $key == '8';
2091
         *  } );
2092
         *
2093
         * Results:
2094
         * Both examples will return "1" because the value "b" is at the second position
2095
         * and the returned index is zero based so the first item has the index "0".
2096
         *
2097
         * @param \Closure|string|int $value Key to search for or function with (key) parameters return TRUE if key is found
2098
         * @return int|null Position of the found value (zero based) or NULL if not found
2099
         */
2100
        public function index( $value ) : ?int
2101
        {
2102
                if( $value instanceof \Closure )
28✔
2103
                {
2104
                        $pos = 0;
14✔
2105

2106
                        foreach( $this->list() as $key => $item )
14✔
2107
                        {
2108
                                if( $value( $key ) ) {
7✔
2109
                                        return $pos;
7✔
2110
                                }
2111

2112
                                ++$pos;
7✔
2113
                        }
2114

2115
                        return null;
7✔
2116
                }
2117

2118
                $pos = array_search( $value, array_keys( $this->list() ) );
14✔
2119
                return $pos !== false ? $pos : null;
14✔
2120
        }
2121

2122

2123
        /**
2124
         * Inserts the value or values after the given element.
2125
         *
2126
         * Examples:
2127
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAfter( 'foo', 'baz' );
2128
         *  Map::from( ['foo', 'bar'] )->insertAfter( 'foo', ['baz', 'boo'] );
2129
         *  Map::from( ['foo', 'bar'] )->insertAfter( null, 'baz' );
2130
         *
2131
         * Results:
2132
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2133
         *  ['foo', 'baz', 'boo', 'bar']
2134
         *  ['foo', 'bar', 'baz']
2135
         *
2136
         * Numerical array indexes are not preserved.
2137
         *
2138
         * @param mixed $element Element after the value is inserted
2139
         * @param mixed $value Element or list of elements to insert
2140
         * @return self<int|string,mixed> Updated map for fluid interface
2141
         */
2142
        public function insertAfter( $element, $value ) : self
2143
        {
2144
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
21✔
2145
                array_splice( $this->list(), $position + 1, 0, $this->array( $value ) );
21✔
2146

2147
                return $this;
21✔
2148
        }
2149

2150

2151
        /**
2152
         * Inserts the item at the given position in the map.
2153
         *
2154
         * Examples:
2155
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 0, 'baz' );
2156
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 1, 'baz', 'c' );
2157
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 4, 'baz' );
2158
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( -1, 'baz', 'c' );
2159
         *
2160
         * Results:
2161
         *  [0 => 'baz', 'a' => 'foo', 'b' => 'bar']
2162
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2163
         *  ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']
2164
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2165
         *
2166
         * @param int $pos Position the element it should be inserted at
2167
         * @param mixed $element Element to be inserted
2168
         * @param mixed|null $key Element key or NULL to assign an integer key automatically
2169
         * @return self<int|string,mixed> Updated map for fluid interface
2170
         */
2171
        public function insertAt( int $pos, $element, $key = null ) : self
2172
        {
2173
                if( $key !== null )
35✔
2174
                {
2175
                        $list = $this->list();
14✔
2176

2177
                        $this->list = array_merge(
14✔
2178
                                array_slice( $list, 0, $pos, true ),
14✔
2179
                                [$key => $element],
14✔
2180
                                array_slice( $list, $pos, null, true )
14✔
2181
                        );
10✔
2182
                }
2183
                else
2184
                {
2185
                        array_splice( $this->list(), $pos, 0, [$element] );
21✔
2186
                }
2187

2188
                return $this;
35✔
2189
        }
2190

2191

2192
        /**
2193
         * Inserts the value or values before the given element.
2194
         *
2195
         * Examples:
2196
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertBefore( 'bar', 'baz' );
2197
         *  Map::from( ['foo', 'bar'] )->insertBefore( 'bar', ['baz', 'boo'] );
2198
         *  Map::from( ['foo', 'bar'] )->insertBefore( null, 'baz' );
2199
         *
2200
         * Results:
2201
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2202
         *  ['foo', 'baz', 'boo', 'bar']
2203
         *  ['foo', 'bar', 'baz']
2204
         *
2205
         * Numerical array indexes are not preserved.
2206
         *
2207
         * @param mixed $element Element before the value is inserted
2208
         * @param mixed $value Element or list of elements to insert
2209
         * @return self<int|string,mixed> Updated map for fluid interface
2210
         */
2211
        public function insertBefore( $element, $value ) : self
2212
        {
2213
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
21✔
2214
                array_splice( $this->list(), $position, 0, $this->array( $value ) );
21✔
2215

2216
                return $this;
21✔
2217
        }
2218

2219

2220
        /**
2221
         * Tests if the passed value or values are part of the strings in the map.
2222
         *
2223
         * Examples:
2224
         *  Map::from( ['abc'] )->inString( 'c' );
2225
         *  Map::from( ['abc'] )->inString( 'bc' );
2226
         *  Map::from( [12345] )->inString( '23' );
2227
         *  Map::from( [123.4] )->inString( 23.4 );
2228
         *  Map::from( [12345] )->inString( false );
2229
         *  Map::from( [12345] )->inString( true );
2230
         *  Map::from( [false] )->inString( false );
2231
         *  Map::from( ['abc'] )->inString( '' );
2232
         *  Map::from( [''] )->inString( false );
2233
         *  Map::from( ['abc'] )->inString( 'BC', false );
2234
         *  Map::from( ['abc', 'def'] )->inString( ['de', 'xy'] );
2235
         *  Map::from( ['abc', 'def'] )->inString( ['E', 'x'] );
2236
         *  Map::from( ['abc', 'def'] )->inString( 'E' );
2237
         *  Map::from( [23456] )->inString( true );
2238
         *  Map::from( [false] )->inString( 0 );
2239
         *
2240
         * Results:
2241
         * The first eleven examples will return TRUE while the last four will return FALSE
2242
         *
2243
         * All scalar values (bool, float, int and string) are casted to string values before
2244
         * comparing to the given value. Non-scalar values in the map are ignored.
2245
         *
2246
         * @param array|string $value Value or values to compare the map elements, will be casted to string type
2247
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
2248
         * @return bool TRUE If at least one element matches, FALSE if value is not in any string of the map
2249
         * @deprecated Use multi-byte aware strContains() instead
2250
         */
2251
        public function inString( $value, bool $case = true ) : bool
2252
        {
2253
                $fcn = $case ? 'strpos' : 'stripos';
7✔
2254

2255
                foreach( (array) $value as $val )
7✔
2256
                {
2257
                        if( (string) $val === '' ) {
7✔
2258
                                return true;
7✔
2259
                        }
2260

2261
                        foreach( $this->list() as $item )
7✔
2262
                        {
2263
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
7✔
2264
                                        return true;
7✔
2265
                                }
2266
                        }
2267
                }
2268

2269
                return false;
7✔
2270
        }
2271

2272

2273
        /**
2274
         * Returns an element by key and casts it to integer if possible.
2275
         *
2276
         * Examples:
2277
         *  Map::from( ['a' => true] )->int( 'a' );
2278
         *  Map::from( ['a' => '1'] )->int( 'a' );
2279
         *  Map::from( ['a' => 1.1] )->int( 'a' );
2280
         *  Map::from( ['a' => '10'] )->int( 'a' );
2281
         *  Map::from( ['a' => ['b' => ['c' => 1]]] )->int( 'a/b/c' );
2282
         *  Map::from( [] )->int( 'c', function() { return rand( 1, 1 ); } );
2283
         *  Map::from( [] )->int( 'a', 1 );
2284
         *
2285
         *  Map::from( [] )->int( 'b' );
2286
         *  Map::from( ['b' => ''] )->int( 'b' );
2287
         *  Map::from( ['b' => 'abc'] )->int( 'b' );
2288
         *  Map::from( ['b' => null] )->int( 'b' );
2289
         *  Map::from( ['b' => [1]] )->int( 'b' );
2290
         *  Map::from( ['b' => #resource] )->int( 'b' );
2291
         *  Map::from( ['b' => new \stdClass] )->int( 'b' );
2292
         *
2293
         *  Map::from( [] )->int( 'c', new \Exception( 'error' ) );
2294
         *
2295
         * Results:
2296
         * The first seven examples will return 1 while the 8th to 14th example
2297
         * returns 0. The last example will throw an exception.
2298
         *
2299
         * This does also work for multi-dimensional arrays by passing the keys
2300
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2301
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2302
         * public properties of objects or objects implementing __isset() and __get() methods.
2303
         *
2304
         * @param int|string $key Key or path to the requested item
2305
         * @param mixed $default Default value if key isn't found (will be casted to integer)
2306
         * @return int Value from map or default value
2307
         */
2308
        public function int( $key, $default = 0 ) : int
2309
        {
2310
                return (int) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
21✔
2311
        }
2312

2313

2314
        /**
2315
         * Returns all values in a new map that are available in both, the map and the given elements.
2316
         *
2317
         * Examples:
2318
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersect( ['bar'] );
2319
         *
2320
         * Results:
2321
         *  ['b' => 'bar']
2322
         *
2323
         * If a callback is passed, the given function will be used to compare the values.
2324
         * The function must accept two parameters (value A and B) and must return
2325
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2326
         * greater than value B. Both, a method name and an anonymous function can be passed:
2327
         *
2328
         *  Map::from( [0 => 'a'] )->intersect( [0 => 'A'], 'strcasecmp' );
2329
         *  Map::from( ['b' => 'a'] )->intersect( ['B' => 'A'], 'strcasecmp' );
2330
         *  Map::from( ['b' => 'a'] )->intersect( ['c' => 'A'], function( $valA, $valB ) {
2331
         *      return strtolower( $valA ) <=> strtolower( $valB );
2332
         *  } );
2333
         *
2334
         * All examples will return a map containing ['a'] because both contain the same
2335
         * values when compared case insensitive.
2336
         *
2337
         * The keys are preserved using this method.
2338
         *
2339
         * @param iterable<int|string,mixed> $elements List of elements
2340
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2341
         * @return self<int|string,mixed> New map
2342
         */
2343
        public function intersect( iterable $elements, callable $callback = null ) : self
2344
        {
2345
                $list = $this->list();
14✔
2346
                $elements = $this->array( $elements );
14✔
2347

2348
                if( $callback ) {
14✔
2349
                        return new static( array_uintersect( $list, $elements, $callback ) );
7✔
2350
                }
2351

2352
                // using array_intersect() is 7x slower
2353
                return ( new static( $list ) )
7✔
2354
                        ->remove( array_keys( array_diff( $list, $elements ) ) )
7✔
2355
                        ->remove( array_keys( array_diff( $elements, $list ) ) );
7✔
2356
        }
2357

2358

2359
        /**
2360
         * Returns all values in a new map that are available in both, the map and the given elements while comparing the keys too.
2361
         *
2362
         * Examples:
2363
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectAssoc( new Map( ['foo', 'b' => 'bar'] ) );
2364
         *
2365
         * Results:
2366
         *  ['a' => 'foo']
2367
         *
2368
         * If a callback is passed, the given function will be used to compare the values.
2369
         * The function must accept two parameters (value A and B) and must return
2370
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2371
         * greater than value B. Both, a method name and an anonymous function can be passed:
2372
         *
2373
         *  Map::from( [0 => 'a'] )->intersectAssoc( [0 => 'A'], 'strcasecmp' );
2374
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['B' => 'A'], 'strcasecmp' );
2375
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['c' => 'A'], function( $valA, $valB ) {
2376
         *      return strtolower( $valA ) <=> strtolower( $valB );
2377
         *  } );
2378
         *
2379
         * The first example will return [0 => 'a'] because both contain the same
2380
         * values when compared case insensitive. The second and third example will return
2381
         * an empty map because the keys doesn't match ("b" vs. "B" and "b" vs. "c").
2382
         *
2383
         * The keys are preserved using this method.
2384
         *
2385
         * @param iterable<int|string,mixed> $elements List of elements
2386
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2387
         * @return self<int|string,mixed> New map
2388
         */
2389
        public function intersectAssoc( iterable $elements, callable $callback = null ) : self
2390
        {
2391
                $elements = $this->array( $elements );
35✔
2392

2393
                if( $callback ) {
35✔
2394
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
7✔
2395
                }
2396

2397
                return new static( array_intersect_assoc( $this->list(), $elements ) );
28✔
2398
        }
2399

2400

2401
        /**
2402
         * Returns all values in a new map that are available in both, the map and the given elements by comparing the keys only.
2403
         *
2404
         * Examples:
2405
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectKeys( new Map( ['foo', 'b' => 'baz'] ) );
2406
         *
2407
         * Results:
2408
         *  ['b' => 'bar']
2409
         *
2410
         * If a callback is passed, the given function will be used to compare the keys.
2411
         * The function must accept two parameters (key A and B) and must return
2412
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
2413
         * greater than key B. Both, a method name and an anonymous function can be passed:
2414
         *
2415
         *  Map::from( [0 => 'a'] )->intersectKeys( [0 => 'A'], 'strcasecmp' );
2416
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['B' => 'X'], 'strcasecmp' );
2417
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['c' => 'a'], function( $keyA, $keyB ) {
2418
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
2419
         *  } );
2420
         *
2421
         * The first example will return a map with [0 => 'a'] and the second one will
2422
         * return a map with ['b' => 'a'] because both contain the same keys when compared
2423
         * case insensitive. The third example will return an empty map because the keys
2424
         * doesn't match ("b" vs. "c").
2425
         *
2426
         * The keys are preserved using this method.
2427
         *
2428
         * @param iterable<int|string,mixed> $elements List of elements
2429
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
2430
         * @return self<int|string,mixed> New map
2431
         */
2432
        public function intersectKeys( iterable $elements, callable $callback = null ) : self
2433
        {
2434
                $list = $this->list();
21✔
2435
                $elements = $this->array( $elements );
21✔
2436

2437
                if( $callback ) {
21✔
2438
                        return new static( array_intersect_ukey( $list, $elements, $callback ) );
7✔
2439
                }
2440

2441
                // using array_intersect_key() is 1.6x slower
2442
                return ( new static( $list ) )
14✔
2443
                        ->remove( array_keys( array_diff_key( $list, $elements ) ) )
14✔
2444
                        ->remove( array_keys( array_diff_key( $elements, $list ) ) );
14✔
2445
        }
2446

2447

2448
        /**
2449
         * Tests if the map consists of the same keys and values
2450
         *
2451
         * Examples:
2452
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'] );
2453
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'], true );
2454
         *  Map::from( [1, 2] )->is( ['1', '2'] );
2455
         *
2456
         * Results:
2457
         *  The first example returns TRUE while the second and third one returns FALSE
2458
         *
2459
         * @param iterable<int|string,mixed> $list List of key/value pairs to compare with
2460
         * @param bool $strict TRUE for comparing order of elements too, FALSE for key/values only
2461
         * @return bool TRUE if given list is equal, FALSE if not
2462
         */
2463
        public function is( iterable $list, bool $strict = false ) : bool
2464
        {
2465
                $list = $this->array( $list );
21✔
2466

2467
                if( $strict ) {
21✔
2468
                        return $this->list() === $list;
14✔
2469
                }
2470

2471
                return $this->list() == $list;
7✔
2472
        }
2473

2474

2475
        /**
2476
         * Determines if the map is empty or not.
2477
         *
2478
         * Examples:
2479
         *  Map::from( [] )->isEmpty();
2480
         *  Map::from( ['a'] )->isEmpty();
2481
         *
2482
         * Results:
2483
         *  The first example returns TRUE while the second returns FALSE
2484
         *
2485
         * The method is equivalent to empty().
2486
         *
2487
         * @return bool TRUE if map is empty, FALSE if not
2488
         */
2489
        public function isEmpty() : bool
2490
        {
2491
                return empty( $this->list() );
28✔
2492
        }
2493

2494

2495
        /**
2496
         * Determines if all entries are numeric values.
2497
         *
2498
         * Examples:
2499
         *  Map::from( [] )->isNumeric();
2500
         *  Map::from( [1] )->isNumeric();
2501
         *  Map::from( [1.1] )->isNumeric();
2502
         *  Map::from( [010] )->isNumeric();
2503
         *  Map::from( [0x10] )->isNumeric();
2504
         *  Map::from( [0b10] )->isNumeric();
2505
         *  Map::from( ['010'] )->isNumeric();
2506
         *  Map::from( ['10'] )->isNumeric();
2507
         *  Map::from( ['10.1'] )->isNumeric();
2508
         *  Map::from( [' 10 '] )->isNumeric();
2509
         *  Map::from( ['10e2'] )->isNumeric();
2510
         *  Map::from( ['0b10'] )->isNumeric();
2511
         *  Map::from( ['0x10'] )->isNumeric();
2512
         *  Map::from( ['null'] )->isNumeric();
2513
         *  Map::from( [null] )->isNumeric();
2514
         *  Map::from( [true] )->isNumeric();
2515
         *  Map::from( [[]] )->isNumeric();
2516
         *  Map::from( [''] )->isNumeric();
2517
         *
2518
         * Results:
2519
         *  The first eleven examples return TRUE while the last seven return FALSE
2520
         *
2521
         * @return bool TRUE if all map entries are numeric values, FALSE if not
2522
         */
2523
        public function isNumeric() : bool
2524
        {
2525
                $result = true;
7✔
2526

2527
                foreach( $this->list() as $val )
7✔
2528
                {
2529
                        if( !is_numeric( $val ) ) {
7✔
2530
                                $result = false;
7✔
2531
                        }
2532
                }
2533

2534
                return $result;
7✔
2535
        }
2536

2537

2538
        /**
2539
         * Determines if all entries are objects.
2540
         *
2541
         * Examples:
2542
         *  Map::from( [] )->isObject();
2543
         *  Map::from( [new stdClass] )->isObject();
2544
         *  Map::from( [1] )->isObject();
2545
         *
2546
         * Results:
2547
         *  The first two examples return TRUE while the last one return FALSE
2548
         *
2549
         * @return bool TRUE if all map entries are objects, FALSE if not
2550
         */
2551
        public function isObject() : bool
2552
        {
2553
                $result = true;
7✔
2554

2555
                foreach( $this->list() as $val )
7✔
2556
                {
2557
                        if( !is_object( $val ) ) {
7✔
2558
                                $result = false;
7✔
2559
                        }
2560
                }
2561

2562
                return $result;
7✔
2563
        }
2564

2565

2566
        /**
2567
         * Determines if all entries are scalar values.
2568
         *
2569
         * Examples:
2570
         *  Map::from( [] )->isScalar();
2571
         *  Map::from( [1] )->isScalar();
2572
         *  Map::from( [1.1] )->isScalar();
2573
         *  Map::from( ['abc'] )->isScalar();
2574
         *  Map::from( [true, false] )->isScalar();
2575
         *  Map::from( [new stdClass] )->isScalar();
2576
         *  Map::from( [#resource] )->isScalar();
2577
         *  Map::from( [null] )->isScalar();
2578
         *  Map::from( [[1]] )->isScalar();
2579
         *
2580
         * Results:
2581
         *  The first five examples return TRUE while the others return FALSE
2582
         *
2583
         * @return bool TRUE if all map entries are scalar values, FALSE if not
2584
         */
2585
        public function isScalar() : bool
2586
        {
2587
                $result = true;
7✔
2588

2589
                foreach( $this->list() as $val )
7✔
2590
                {
2591
                        if( !is_scalar( $val ) ) {
7✔
2592
                                $result = false;
7✔
2593
                        }
2594
                }
2595

2596
                return $result;
7✔
2597
        }
2598

2599

2600
        /**
2601
         * Concatenates the string representation of all elements.
2602
         *
2603
         * Objects that implement __toString() does also work, otherwise (and in case
2604
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2605
         * empty strings.
2606
         *
2607
         * Examples:
2608
         *  Map::from( ['a', 'b', false] )->join();
2609
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
2610
         *
2611
         * Results:
2612
         * The first example will return "ab" while the second one will return "a-b--"
2613
         *
2614
         * @param string $glue Character or string added between elements
2615
         * @return string String of concatenated map elements
2616
         */
2617
        public function join( string $glue = '' ) : string
2618
        {
2619
                return implode( $glue, $this->list() );
7✔
2620
        }
2621

2622

2623
        /**
2624
         * Specifies the data which should be serialized to JSON by json_encode().
2625
         *
2626
         * Examples:
2627
         *   json_encode( Map::from( ['a', 'b'] ) );
2628
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
2629
         *
2630
         * Results:
2631
         *   ["a", "b"]
2632
         *   {"a":0,"b":1}
2633
         *
2634
         * @return array<int|string,mixed> Data to serialize to JSON
2635
         */
2636
        #[\ReturnTypeWillChange]
2637
        public function jsonSerialize()
2638
        {
2639
                return $this->list = $this->array( $this->list );
7✔
2640
        }
2641

2642

2643
        /**
2644
         * Returns the keys of the all elements in a new map object.
2645
         *
2646
         * Examples:
2647
         *  Map::from( ['a', 'b'] );
2648
         *  Map::from( ['a' => 0, 'b' => 1] );
2649
         *
2650
         * Results:
2651
         * The first example returns a map containing [0, 1] while the second one will
2652
         * return a map with ['a', 'b'].
2653
         *
2654
         * @return self<int|string,mixed> New map
2655
         */
2656
        public function keys() : self
2657
        {
2658
                return new static( array_keys( $this->list() ) );
7✔
2659
        }
2660

2661

2662
        /**
2663
         * Sorts the elements by their keys in reverse order.
2664
         *
2665
         * Examples:
2666
         *  Map::from( ['b' => 0, 'a' => 1] )->krsort();
2667
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsort();
2668
         *
2669
         * Results:
2670
         *  ['a' => 1, 'b' => 0]
2671
         *  [0 => 'b', 1 => 'a']
2672
         *
2673
         * The parameter modifies how the keys are compared. Possible values are:
2674
         * - SORT_REGULAR : compare elements normally (don't change types)
2675
         * - SORT_NUMERIC : compare elements numerically
2676
         * - SORT_STRING : compare elements as strings
2677
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2678
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2679
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2680
         *
2681
         * The keys are preserved using this method and no new map is created.
2682
         *
2683
         * @param int $options Sort options for krsort()
2684
         * @return self<int|string,mixed> Updated map for fluid interface
2685
         */
2686
        public function krsort( int $options = SORT_REGULAR ) : self
2687
        {
2688
                krsort( $this->list(), $options );
14✔
2689
                return $this;
14✔
2690
        }
2691

2692

2693
        /**
2694
         * Sorts the elements by their keys.
2695
         *
2696
         * Examples:
2697
         *  Map::from( ['b' => 0, 'a' => 1] )->ksort();
2698
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksort();
2699
         *
2700
         * Results:
2701
         *  ['a' => 1, 'b' => 0]
2702
         *  [0 => 'b', 1 => 'a']
2703
         *
2704
         * The parameter modifies how the keys are compared. Possible values are:
2705
         * - SORT_REGULAR : compare elements normally (don't change types)
2706
         * - SORT_NUMERIC : compare elements numerically
2707
         * - SORT_STRING : compare elements as strings
2708
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2709
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2710
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2711
         *
2712
         * The keys are preserved using this method and no new map is created.
2713
         *
2714
         * @param int $options Sort options for ksort()
2715
         * @return self<int|string,mixed> Updated map for fluid interface
2716
         */
2717
        public function ksort( int $options = SORT_REGULAR ) : self
2718
        {
2719
                ksort( $this->list(), $options );
14✔
2720
                return $this;
14✔
2721
        }
2722

2723

2724
        /**
2725
         * Returns the last element from the map.
2726
         *
2727
         * Examples:
2728
         *  Map::from( ['a', 'b'] )->last();
2729
         *  Map::from( [] )->last( 'x' );
2730
         *  Map::from( [] )->last( new \Exception( 'error' ) );
2731
         *  Map::from( [] )->last( function() { return rand(); } );
2732
         *
2733
         * Results:
2734
         * The first example will return 'b' and the second one 'x'. The third example
2735
         * will throw the exception passed if the map contains no elements. In the
2736
         * fourth example, a random value generated by the closure function will be
2737
         * returned.
2738
         *
2739
         * @param mixed $default Default value or exception if the map contains no elements
2740
         * @return mixed Last value of map, (generated) default value or an exception
2741
         */
2742
        public function last( $default = null )
2743
        {
2744
                if( ( $value = end( $this->list() ) ) !== false ) {
35✔
2745
                        return $value;
14✔
2746
                }
2747

2748
                if( $default instanceof \Closure ) {
21✔
2749
                        return $default();
7✔
2750
                }
2751

2752
                if( $default instanceof \Throwable ) {
14✔
2753
                        throw $default;
7✔
2754
                }
2755

2756
                return $default;
7✔
2757
        }
2758

2759

2760
        /**
2761
         * Returns the last key from the map.
2762
         *
2763
         * Examples:
2764
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
2765
         *  Map::from( [] )->lastKey();
2766
         *
2767
         * Results:
2768
         * The first example will return 'b' and the second one NULL.
2769
         *
2770
         * @return mixed Last key of map or NULL if empty
2771
         */
2772
        public function lastKey()
2773
        {
2774
                $list = $this->list();
14✔
2775

2776
                if( function_exists( 'array_key_last' ) ) {
14✔
2777
                        return array_key_last( $list );
14✔
2778
                }
2779

2780
                end( $list );
×
2781
                return key( $list );
×
2782
        }
2783

2784

2785
        /**
2786
         * Removes the passed characters from the left of all strings.
2787
         *
2788
         * Examples:
2789
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
2790
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
2791
         *
2792
         * Results:
2793
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
2794
         *
2795
         * @param string $chars List of characters to trim
2796
         * @return self<int|string,mixed> Updated map for fluid interface
2797
         */
2798
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
2799
        {
2800
                foreach( $this->list() as &$entry )
7✔
2801
                {
2802
                        if( is_string( $entry ) ) {
7✔
2803
                                $entry = ltrim( $entry, $chars );
7✔
2804
                        }
2805
                }
2806

2807
                return $this;
7✔
2808
        }
2809

2810

2811
        /**
2812
         * Calls the passed function once for each element and returns a new map for the result.
2813
         *
2814
         * Examples:
2815
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
2816
         *      return $value * 2;
2817
         *  } );
2818
         *
2819
         * Results:
2820
         *  ['a' => 4, 'b' => 8]
2821
         *
2822
         * The keys are preserved using this method.
2823
         *
2824
         * @param callable $callback Function with (value, key) parameters and returns computed result
2825
         * @return self<int|string,mixed> New map with the original keys and the computed values
2826
         */
2827
        public function map( callable $callback ) : self
2828
        {
2829
                $list = $this->list();
7✔
2830
                $keys = array_keys( $list );
7✔
2831
                $elements = array_map( $callback, $list, $keys );
7✔
2832

2833
                return new static( array_combine( $keys, $elements ) ?: [] );
7✔
2834
        }
2835

2836

2837
        /**
2838
         * Returns the maximum value of all elements.
2839
         *
2840
         * Examples:
2841
         *  Map::from( [1, 3, 2, 5, 4] )->max()
2842
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
2843
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
2844
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
2845
         *
2846
         * Results:
2847
         * The first line will return "5", the second one "foo" and the third/fourth
2848
         * one return both 50.
2849
         *
2850
         * This does also work for multi-dimensional arrays by passing the keys
2851
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2852
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2853
         * public properties of objects or objects implementing __isset() and __get() methods.
2854
         *
2855
         * Be careful comparing elements of different types because this can have
2856
         * unpredictable results due to the PHP comparison rules:
2857
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
2858
         *
2859
         * @param string|null $key Key or path to the value of the nested array or object
2860
         * @return mixed Maximum value or NULL if there are no elements in the map
2861
         */
2862
        public function max( string $key = null )
2863
        {
2864
                $vals = $key !== null ? $this->col( $key )->toArray() : $this->list();
21✔
2865
                return !empty( $vals ) ? max( $vals ) : null;
21✔
2866
        }
2867

2868

2869
        /**
2870
         * Merges the map with the given elements without returning a new map.
2871
         *
2872
         * Elements with the same non-numeric keys will be overwritten, elements
2873
         * with the same numeric keys will be added.
2874
         *
2875
         * Examples:
2876
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
2877
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
2878
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
2879
         *
2880
         * Results:
2881
         *  ['a', 'b', 'b', 'c']
2882
         *  ['a' => 1, 'b' => 4, 'c' => 6]
2883
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
2884
         *
2885
         * The method is similar to replace() but doesn't replace elements with
2886
         * the same numeric keys. If you want to be sure that all passed elements
2887
         * are added without replacing existing ones, use concat() instead.
2888
         *
2889
         * The keys are preserved using this method.
2890
         *
2891
         * @param iterable<int|string,mixed> $elements List of elements
2892
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
2893
         * @return self<int|string,mixed> Updated map for fluid interface
2894
         */
2895
        public function merge( iterable $elements, bool $recursive = false ) : self
2896
        {
2897
                if( $recursive ) {
21✔
2898
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
7✔
2899
                } else {
2900
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
14✔
2901
                }
2902

2903
                return $this;
21✔
2904
        }
2905

2906

2907
        /**
2908
         * Returns the minimum value of all elements.
2909
         *
2910
         * Examples:
2911
         *  Map::from( [2, 3, 1, 5, 4] )->min()
2912
         *  Map::from( ['baz', 'foo', 'bar'] )->min()
2913
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->min( 'p' )
2914
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( 'i/p' )
2915
         *
2916
         * Results:
2917
         * The first line will return "1", the second one "bar", the third one
2918
         * returns 10 while the last one returns 30.
2919
         *
2920
         * This does also work for multi-dimensional arrays by passing the keys
2921
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2922
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2923
         * public properties of objects or objects implementing __isset() and __get() methods.
2924
         *
2925
         * Be careful comparing elements of different types because this can have
2926
         * unpredictable results due to the PHP comparison rules:
2927
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
2928
         *
2929
         * @param string|null $key Key or path to the value of the nested array or object
2930
         * @return mixed Minimum value or NULL if there are no elements in the map
2931
         */
2932
        public function min( string $key = null )
2933
        {
2934
                $vals = $key !== null ? $this->col( $key )->toArray() : $this->list();
21✔
2935
                return !empty( $vals ) ? min( $vals ) : null;
21✔
2936
        }
2937

2938

2939
        /**
2940
         * Tests if none of the elements are part of the map.
2941
         *
2942
         * Examples:
2943
         *  Map::from( ['a', 'b'] )->none( 'x' );
2944
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
2945
         *  Map::from( ['1', '2'] )->none( 2, true );
2946
         *  Map::from( ['a', 'b'] )->none( 'a' );
2947
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
2948
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
2949
         *
2950
         * Results:
2951
         * The first three examples will return TRUE while the other ones will return FALSE
2952
         *
2953
         * @param mixed|array $element Element or elements to search for in the map
2954
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2955
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
2956
         */
2957
        public function none( $element, bool $strict = false ) : bool
2958
        {
2959
                $list = $this->list();
7✔
2960

2961
                if( !is_array( $element ) ) {
7✔
2962
                        return !in_array( $element, $list, $strict );
7✔
2963
                };
2964

2965
                foreach( $element as $entry )
7✔
2966
                {
2967
                        if( in_array( $entry, $list, $strict ) === true ) {
7✔
2968
                                return false;
7✔
2969
                        }
2970
                }
2971

2972
                return true;
7✔
2973
        }
2974

2975

2976
        /**
2977
         * Returns every nth element from the map.
2978
         *
2979
         * Examples:
2980
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
2981
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
2982
         *
2983
         * Results:
2984
         *  ['a', 'c', 'e']
2985
         *  ['b', 'd', 'f']
2986
         *
2987
         * @param int $step Step width
2988
         * @param int $offset Number of element to start from (0-based)
2989
         * @return self<int|string,mixed> New map
2990
         */
2991
        public function nth( int $step, int $offset = 0 ) : self
2992
        {
2993
                $pos = 0;
7✔
2994
                $result = [];
7✔
2995

2996
                foreach( $this->list() as $key => $item )
7✔
2997
                {
2998
                        if( $pos++ % $step === $offset ) {
7✔
2999
                                $result[$key] = $item;
7✔
3000
                        }
3001
                }
3002

3003
                return new static( $result );
7✔
3004
        }
3005

3006

3007
        /**
3008
         * Determines if an element exists at an offset.
3009
         *
3010
         * Examples:
3011
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3012
         *  isset( $map['b'] );
3013
         *  isset( $map['c'] );
3014
         *  isset( $map['d'] );
3015
         *
3016
         * Results:
3017
         *  The first isset() will return TRUE while the second and third one will return FALSE
3018
         *
3019
         * @param int|string $key Key to check for
3020
         * @return bool TRUE if key exists, FALSE if not
3021
         */
3022
        public function offsetExists( $key ) : bool
3023
        {
3024
                return isset( $this->list()[$key] );
49✔
3025
        }
3026

3027

3028
        /**
3029
         * Returns an element at a given offset.
3030
         *
3031
         * Examples:
3032
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3033
         *  $map['b'];
3034
         *
3035
         * Results:
3036
         *  $map['b'] will return 3
3037
         *
3038
         * @param int|string $key Key to return the element for
3039
         * @return mixed Value associated to the given key
3040
         */
3041
        #[\ReturnTypeWillChange]
3042
        public function offsetGet( $key )
3043
        {
3044
                return $this->list()[$key] ?? null;
35✔
3045
        }
3046

3047

3048
        /**
3049
         * Sets the element at a given offset.
3050
         *
3051
         * Examples:
3052
         *  $map = Map::from( ['a' => 1] );
3053
         *  $map['b'] = 2;
3054
         *  $map[0] = 4;
3055
         *
3056
         * Results:
3057
         *  ['a' => 1, 'b' => 2, 0 => 4]
3058
         *
3059
         * @param int|string|null $key Key to set the element for or NULL to append value
3060
         * @param mixed $value New value set for the key
3061
         */
3062
        public function offsetSet( $key, $value ) : void
3063
        {
3064
                if( $key !== null ) {
21✔
3065
                        $this->list()[$key] = $value;
14✔
3066
                } else {
3067
                        $this->list()[] = $value;
14✔
3068
                }
3069
        }
6✔
3070

3071

3072
        /**
3073
         * Unsets the element at a given offset.
3074
         *
3075
         * Examples:
3076
         *  $map = Map::from( ['a' => 1] );
3077
         *  unset( $map['a'] );
3078
         *
3079
         * Results:
3080
         *  The map will be empty
3081
         *
3082
         * @param int|string $key Key for unsetting the item
3083
         */
3084
        public function offsetUnset( $key ) : void
3085
        {
3086
                unset( $this->list()[$key] );
14✔
3087
        }
4✔
3088

3089

3090
        /**
3091
         * Returns a new map with only those elements specified by the given keys.
3092
         *
3093
         * Examples:
3094
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3095
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3096
         *
3097
         * Results:
3098
         *  ['a' => 1]
3099
         *  [0 => 'b', 1 => 'c']
3100
         *
3101
         * The keys are preserved using this method.
3102
         *
3103
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3104
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3105
         */
3106
        public function only( $keys ) : self
3107
        {
3108
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
7✔
3109
        }
3110

3111

3112
        /**
3113
         * Returns a new map with elements ordered by the passed keys.
3114
         *
3115
         * If there are less keys passed than available in the map, the remaining
3116
         * elements are removed. Otherwise, if keys are passed that are not in the
3117
         * map, they will be also available in the returned map but their value is
3118
         * NULL.
3119
         *
3120
         * Examples:
3121
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3122
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3123
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3124
         *
3125
         * Results:
3126
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3127
         *  [0 => 'b', 1 => 'c', 2 => null]
3128
         *  [0 => 'b', 1 => 'c']
3129
         *
3130
         * The keys are preserved using this method.
3131
         *
3132
         * @param iterable<mixed> $keys Keys of the elements in the required order
3133
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3134
         */
3135
        public function order( iterable $keys ) : self
3136
        {
3137
                $result = [];
7✔
3138
                $list = $this->list();
7✔
3139

3140
                foreach( $this->array( $keys ) as $key ) {
7✔
3141
                        $result[$key] = $list[$key] ?? null;
7✔
3142
                }
3143

3144
                return new static( $result );
7✔
3145
        }
3146

3147

3148
        /**
3149
         * Fill up to the specified length with the given value
3150
         *
3151
         * In case the given number is smaller than the number of element that are
3152
         * already in the list, the map is unchanged. If the size is positive, the
3153
         * new elements are padded on the right, if it's negative then the elements
3154
         * are padded on the left.
3155
         *
3156
         * Examples:
3157
         *  Map::from( [1, 2, 3] )->pad( 5 );
3158
         *  Map::from( [1, 2, 3] )->pad( -5 );
3159
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3160
         *  Map::from( [1, 2, 3] )->pad( 2 );
3161
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3162
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3163
         *
3164
         * Results:
3165
         *  [1, 2, 3, null, null]
3166
         *  [null, null, 1, 2, 3]
3167
         *  [1, 2, 3, '0', '0']
3168
         *  [1, 2, 3]
3169
         *  [0 => 1, 1 => 2, 2 => null]
3170
         *  ['a' => 1, 'b' => 2, 0 => 3]
3171
         *
3172
         * Associative keys are preserved, numerical keys are replaced and numerical
3173
         * keys are used for the new elements.
3174
         *
3175
         * @param int $size Total number of elements that should be in the list
3176
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3177
         * @return self<int|string,mixed> New map
3178
         */
3179
        public function pad( int $size, $value = null ) : self
3180
        {
3181
                return new static( array_pad( $this->list(), $size, $value ) );
7✔
3182
        }
3183

3184

3185
        /**
3186
         * Breaks the list of elements into the given number of groups.
3187
         *
3188
         * Examples:
3189
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3190
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3191
         *                return $idx % 3;
3192
         *        } );
3193
         *
3194
         * Results:
3195
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3196
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3197
         *
3198
         * The keys of the original map are preserved in the returned map.
3199
         *
3200
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3201
         * @return self<int|string,mixed> New map
3202
         */
3203
        public function partition( $number ) : self
3204
        {
3205
                $list = $this->list();
28✔
3206

3207
                if( empty( $list ) ) {
28✔
3208
                        return new static();
7✔
3209
                }
3210

3211
                $result = [];
21✔
3212

3213
                if( $number instanceof \Closure )
21✔
3214
                {
3215
                        foreach( $list as $idx => $item ) {
7✔
3216
                                $result[$number( $item, $idx )][$idx] = $item;
7✔
3217
                        }
3218

3219
                        return new static( $result );
7✔
3220
                }
3221
                elseif( is_int( $number ) )
14✔
3222
                {
3223
                        $start = 0;
7✔
3224
                        $size = (int) ceil( count( $list ) / $number );
7✔
3225

3226
                        for( $i = 0; $i < $number; $i++ )
7✔
3227
                        {
3228
                                $result[] = array_slice( $list, $start, $size, true );
7✔
3229
                                $start += $size;
7✔
3230
                        }
3231

3232
                        return new static( $result );
7✔
3233
                }
3234

3235
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
7✔
3236
        }
3237

3238

3239
        /**
3240
         * Passes the map to the given callback and return the result.
3241
         *
3242
         * Examples:
3243
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3244
         *      return join( '-', $map->toArray() );
3245
         *  } );
3246
         *
3247
         * Results:
3248
         *  "a-b" will be returned
3249
         *
3250
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3251
         * @return mixed Result returned by the callback
3252
         */
3253
        public function pipe( \Closure $callback )
3254
        {
3255
                return $callback( $this );
7✔
3256
        }
3257

3258

3259
        /**
3260
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3261
         *
3262
         * This method is an alias for col(). For performance reasons, col() should
3263
         * be preferred because it uses one method call less than pluck().
3264
         *
3265
         * @param string|null $valuecol Name or path of the value property
3266
         * @param string|null $indexcol Name or path of the index property
3267
         * @return self<int|string,mixed> New map with mapped entries
3268
         */
3269
        public function pluck( string $valuecol = null, string $indexcol = null ) : self
3270
        {
3271
                return $this->col( $valuecol, $indexcol );
7✔
3272
        }
3273

3274

3275
        /**
3276
         * Returns and removes the last element from the map.
3277
         *
3278
         * Examples:
3279
         *  Map::from( ['a', 'b'] )->pop();
3280
         *
3281
         * Results:
3282
         *  "b" will be returned and the map only contains ['a'] afterwards
3283
         *
3284
         * @return mixed Last element of the map or null if empty
3285
         */
3286
        public function pop()
3287
        {
3288
                return array_pop( $this->list() );
14✔
3289
        }
3290

3291

3292
        /**
3293
         * Returns the numerical index of the value.
3294
         *
3295
         * Examples:
3296
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3297
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3298
         *      return $item === 'b';
3299
         *  } );
3300
         *
3301
         * Results:
3302
         * Both examples will return "1" because the value "b" is at the second position
3303
         * and the returned index is zero based so the first item has the index "0".
3304
         *
3305
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3306
         * @return int|null Position of the found value (zero based) or NULL if not found
3307
         */
3308
        public function pos( $value ) : ?int
3309
        {
3310
                $pos = 0;
105✔
3311
                $list = $this->list();
105✔
3312

3313
                if( $value instanceof \Closure )
105✔
3314
                {
3315
                        foreach( $list as $key => $item )
21✔
3316
                        {
3317
                                if( $value( $item, $key ) ) {
21✔
3318
                                        return $pos;
21✔
3319
                                }
3320

3321
                                ++$pos;
21✔
3322
                        }
3323
                }
3324

3325
                foreach( $list as $key => $item )
84✔
3326
                {
3327
                        if( $item === $value ) {
77✔
3328
                                return $pos;
70✔
3329
                        }
3330

3331
                        ++$pos;
42✔
3332
                }
3333

3334
                return null;
14✔
3335
        }
3336

3337

3338
        /**
3339
         * Adds a prefix in front of each map entry.
3340
         *
3341
         * By defaul, nested arrays are walked recusively so all entries at all levels are prefixed.
3342
         *
3343
         * Examples:
3344
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3345
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3346
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3347
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3348
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3349
         *  } );
3350
         *
3351
         * Results:
3352
         *  The first example returns ['1-a', '1-b'] while the second one will return
3353
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3354
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3355
         *  the closure will return ['145-a', '147-b'].
3356
         *
3357
         * The keys of the original map are preserved in the returned map.
3358
         *
3359
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3360
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3361
         * @return self<int|string,mixed> Updated map for fluid interface
3362
         */
3363
        public function prefix( $prefix, int $depth = null ) : self
3364
        {
3365
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
5✔
3366

3367
                        foreach( $list as $key => $item )
7✔
3368
                        {
3369
                                if( is_array( $item ) ) {
7✔
3370
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
7✔
3371
                                } else {
3372
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
7✔
3373
                                }
3374
                        }
3375

3376
                        return $list;
7✔
3377
                };
7✔
3378

3379
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
7✔
3380
                return $this;
7✔
3381
        }
3382

3383

3384
        /**
3385
         * Pushes an element onto the beginning of the map without returning a new map.
3386
         *
3387
         * This method is an alias for unshift().
3388
         *
3389
         * @param mixed $value Item to add at the beginning
3390
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3391
         * @return self<int|string,mixed> Updated map for fluid interface
3392
         */
3393
        public function prepend( $value, $key = null ) : self
3394
        {
3395
                return $this->unshift( $value, $key );
7✔
3396
        }
3397

3398

3399
        /**
3400
         * Returns and removes an element from the map by its key.
3401
         *
3402
         * Examples:
3403
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3404
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3405
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3406
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3407
         *
3408
         * Results:
3409
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3410
         * The second one will return "none" and the map content stays untouched. If you
3411
         * pass an exception as default value, it will throw that exception if the map
3412
         * contains no elements. In the fourth example, a random value generated by the
3413
         * closure function will be returned.
3414
         *
3415
         * @param int|string $key Key to retrieve the value for
3416
         * @param mixed $default Default value if key isn't available
3417
         * @return mixed Value from map or default value
3418
         */
3419
        public function pull( $key, $default = null )
3420
        {
3421
                $value = $this->get( $key, $default );
28✔
3422
                unset( $this->list()[$key] );
21✔
3423

3424
                return $value;
21✔
3425
        }
3426

3427

3428
        /**
3429
         * Pushes an element onto the end of the map without returning a new map.
3430
         *
3431
         * Examples:
3432
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3433
         *
3434
         * Results:
3435
         *  ['a', 'b', 'aa']
3436
         *
3437
         * @param mixed $value Value to add to the end
3438
         * @return self<int|string,mixed> Updated map for fluid interface
3439
         */
3440
        public function push( $value ) : self
3441
        {
3442
                $this->list()[] = $value;
21✔
3443
                return $this;
21✔
3444
        }
3445

3446

3447
        /**
3448
         * Sets the given key and value in the map without returning a new map.
3449
         *
3450
         * Examples:
3451
         *  Map::from( ['a'] )->put( 1, 'b' );
3452
         *  Map::from( ['a'] )->put( 0, 'b' );
3453
         *
3454
         * Results:
3455
         * The first example results in ['a', 'b'] while the second one produces ['b']
3456
         *
3457
         * This method is an alias for set(). For performance reasons, set() should be
3458
         * preferred because it uses one method call less than put().
3459
         *
3460
         * @param int|string $key Key to set the new value for
3461
         * @param mixed $value New element that should be set
3462
         * @return self<int|string,mixed> Updated map for fluid interface
3463
         */
3464
        public function put( $key, $value ) : self
3465
        {
3466
                return $this->set( $key, $value );
7✔
3467
        }
3468

3469

3470
        /**
3471
         * Returns one or more random element from the map incl. their keys.
3472
         *
3473
         * Examples:
3474
         *  Map::from( [2, 4, 8, 16] )->random();
3475
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3476
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3477
         *
3478
         * Results:
3479
         * The first example will return a map including [0 => 8] or any other value,
3480
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3481
         * and the third example will return a map of the whole list in random order. The
3482
         * less elements are in the map, the less random the order will be, especially if
3483
         * the maximum number of values is high or close to the number of elements.
3484
         *
3485
         * The keys of the original map are preserved in the returned map.
3486
         *
3487
         * @param int $max Maximum number of elements that should be returned
3488
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
3489
         * @throws \InvalidArgumentException If requested number of elements is less than 1
3490
         */
3491
        public function random( int $max = 1 ) : self
3492
        {
3493
                if( $max < 1 ) {
35✔
3494
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
7✔
3495
                }
3496

3497
                $list = $this->list();
28✔
3498

3499
                if( empty( $list ) ) {
28✔
3500
                        return new static();
7✔
3501
                }
3502

3503
                if( ( $num = count( $list ) ) < $max ) {
21✔
3504
                        $max = $num;
7✔
3505
                }
3506

3507
                $keys = array_rand( $list, $max );
21✔
3508

3509
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
21✔
3510
        }
3511

3512

3513
        /**
3514
         * Iteratively reduces the array to a single value using a callback function.
3515
         * Afterwards, the map will be empty.
3516
         *
3517
         * Examples:
3518
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
3519
         *      return $result += $value;
3520
         *  }, 10 );
3521
         *
3522
         * Results:
3523
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
3524
         *
3525
         * @param callable $callback Function with (result, value) parameters and returns result
3526
         * @param mixed $initial Initial value when computing the result
3527
         * @return mixed Value computed by the callback function
3528
         */
3529
        public function reduce( callable $callback, $initial = null )
3530
        {
3531
                return array_reduce( $this->list(), $callback, $initial );
7✔
3532
        }
3533

3534

3535
        /**
3536
         * Removes all matched elements and returns a new map.
3537
         *
3538
         * Examples:
3539
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
3540
         *      return $value < 'm';
3541
         *  } );
3542
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
3543
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
3544
         *
3545
         * Results:
3546
         *  [13 => 'm', 30 => 'z']
3547
         *  [2 => 'a', 30 => 'z']
3548
         *  [6 => null]
3549
         *
3550
         * This method is the inverse of the filter() and should return TRUE if the
3551
         * item should be removed from the returned map.
3552
         *
3553
         * If no callback is passed, all values which are NOT empty, null or false will be
3554
         * removed. The keys of the original map are preserved in the returned map.
3555
         *
3556
         * @param Closure|mixed $callback Function with (item) parameter which returns TRUE/FALSE or value to compare with
3557
         * @return self<int|string,mixed> New map
3558
         */
3559
        public function reject( $callback = true ) : self
3560
        {
3561
                $isCallable = $callback instanceof \Closure;
21✔
3562

3563
                return new static( array_filter( $this->list(), function( $value, $key ) use  ( $callback, $isCallable ) {
15✔
3564
                        return $isCallable ? !$callback( $value, $key ) : $value != $callback;
21✔
3565
                }, ARRAY_FILTER_USE_BOTH ) );
21✔
3566
        }
3567

3568

3569
        /**
3570
         * Changes the keys according to the passed function.
3571
         *
3572
         * Examples:
3573
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
3574
         *      return 'key-' . $key;
3575
         *  } );
3576
         *
3577
         * Results:
3578
         *  ['key-a' => 2, 'key-b' => 4]
3579
         *
3580
         * @param callable $callback Function with (value, key) parameters and returns new key
3581
         * @return self<int|string,mixed> New map with new keys and original values
3582
         */
3583
        public function rekey( callable $callback ) : self
3584
        {
3585
                $list = $this->list();
7✔
3586
                $keys = array_keys( $list );
7✔
3587
                $newKeys = array_map( $callback, $list, $keys );
7✔
3588

3589
                return new static( array_combine( $newKeys, $list ) ?: [] );
7✔
3590
        }
3591

3592

3593
        /**
3594
         * Removes one or more elements from the map by its keys without returning a new map.
3595
         *
3596
         * Examples:
3597
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
3598
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
3599
         *
3600
         * Results:
3601
         * The first example will result in [2 => 'b'] while the second one resulting
3602
         * in an empty list
3603
         *
3604
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
3605
         * @return self<int|string,mixed> Updated map for fluid interface
3606
         */
3607
        public function remove( $keys ) : self
3608
        {
3609
                foreach( $this->array( $keys ) as $key ) {
56✔
3610
                        unset( $this->list()[$key] );
56✔
3611
                }
3612

3613
                return $this;
56✔
3614
        }
3615

3616

3617
        /**
3618
         * Replaces elements in the map with the given elements without returning a new map.
3619
         *
3620
         * Examples:
3621
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
3622
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
3623
         *
3624
         * Results:
3625
         *  ['a' => 2, 2 => 'b']
3626
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
3627
         *
3628
         * The method is similar to merge() but it also replaces elements with numeric
3629
         * keys. These would be added by merge() with a new numeric key.
3630
         *
3631
         * The keys are preserved using this method.
3632
         *
3633
         * @param iterable<int|string,mixed> $elements List of elements
3634
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
3635
         * @return self<int|string,mixed> Updated map for fluid interface
3636
         */
3637
        public function replace( iterable $elements, bool $recursive = true ) : self
3638
        {
3639
                if( $recursive ) {
35✔
3640
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
28✔
3641
                } else {
3642
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
7✔
3643
                }
3644

3645
                return $this;
35✔
3646
        }
3647

3648

3649
        /**
3650
         * Reverses the element order with keys without returning a new map.
3651
         *
3652
         * Examples:
3653
         *  Map::from( ['a', 'b'] )->reverse();
3654
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
3655
         *
3656
         * Results:
3657
         *  ['b', 'a']
3658
         *  ['last' => 'user', 'name' => 'test']
3659
         *
3660
         * The keys are preserved using this method.
3661
         *
3662
         * @return self<int|string,mixed> Updated map for fluid interface
3663
         */
3664
        public function reverse() : self
3665
        {
3666
                $this->list = array_reverse( $this->list(), true );
14✔
3667
                return $this;
14✔
3668
        }
3669

3670

3671
        /**
3672
         * Sorts all elements in reverse order using new keys.
3673
         *
3674
         * Examples:
3675
         *  Map::from( ['a' => 1, 'b' => 0] )->rsort();
3676
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsort();
3677
         *
3678
         * Results:
3679
         *  [0 => 1, 1 => 0]
3680
         *  [0 => 'b', 1 => 'a']
3681
         *
3682
         * The parameter modifies how the values are compared. Possible parameter values are:
3683
         * - SORT_REGULAR : compare elements normally (don't change types)
3684
         * - SORT_NUMERIC : compare elements numerically
3685
         * - SORT_STRING : compare elements as strings
3686
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3687
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3688
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3689
         *
3690
         * The keys aren't preserved and elements get a new index. No new map is created
3691
         *
3692
         * @param int $options Sort options for rsort()
3693
         * @return self<int|string,mixed> Updated map for fluid interface
3694
         */
3695
        public function rsort( int $options = SORT_REGULAR ) : self
3696
        {
3697
                rsort( $this->list(), $options );
14✔
3698
                return $this;
14✔
3699
        }
3700

3701

3702
        /**
3703
         * Removes the passed characters from the right of all strings.
3704
         *
3705
         * Examples:
3706
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
3707
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
3708
         *
3709
         * Results:
3710
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
3711
         *
3712
         * @param string $chars List of characters to trim
3713
         * @return self<int|string,mixed> Updated map for fluid interface
3714
         */
3715
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
3716
        {
3717
                foreach( $this->list() as &$entry )
7✔
3718
                {
3719
                        if( is_string( $entry ) ) {
7✔
3720
                                $entry = rtrim( $entry, $chars );
7✔
3721
                        }
3722
                }
3723

3724
                return $this;
7✔
3725
        }
3726

3727

3728
        /**
3729
         * Searches the map for a given value and return the corresponding key if successful.
3730
         *
3731
         * Examples:
3732
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
3733
         *  Map::from( [1, 2, 3] )->search( '2', true );
3734
         *
3735
         * Results:
3736
         * The first example will return 1 (array index) while the second one will
3737
         * return NULL because the types doesn't match (int vs. string)
3738
         *
3739
         * @param mixed $value Item to search for
3740
         * @param bool $strict TRUE if type of the element should be checked too
3741
         * @return mixed|null Key associated to the value or null if not found
3742
         */
3743
        public function search( $value, $strict = true )
3744
        {
3745
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
7✔
3746
                        return $result;
7✔
3747
                }
3748

3749
                return null;
7✔
3750
        }
3751

3752

3753
        /**
3754
         * Sets the seperator for paths to values in multi-dimensional arrays or objects.
3755
         *
3756
         * This method only changes the separator for the current map instance. To
3757
         * change the separator for all maps created afterwards, use the static
3758
         * delimiter() method instead.
3759
         *
3760
         * Examples:
3761
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '/' )->get( 'foo/bar' );
3762
         *
3763
         * Results:
3764
         *  'baz'
3765
         *
3766
         * @param string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
3767
         * @return self<int|string,mixed> Same map for fluid interface
3768
         */
3769
        public function sep( string $char ) : self
3770
        {
3771
                $this->sep = $char;
7✔
3772
                return $this;
7✔
3773
        }
3774

3775

3776
        /**
3777
         * Sets an element in the map by key without returning a new map.
3778
         *
3779
         * Examples:
3780
         *  Map::from( ['a'] )->set( 1, 'b' );
3781
         *  Map::from( ['a'] )->set( 0, 'b' );
3782
         *
3783
         * Results:
3784
         *  ['a', 'b']
3785
         *  ['b']
3786
         *
3787
         * @param int|string $key Key to set the new value for
3788
         * @param mixed $value New element that should be set
3789
         * @return self<int|string,mixed> Updated map for fluid interface
3790
         */
3791
        public function set( $key, $value ) : self
3792
        {
3793
                $this->list()[(string) $key] = $value;
35✔
3794
                return $this;
35✔
3795
        }
3796

3797

3798
        /**
3799
         * Returns and removes the first element from the map.
3800
         *
3801
         * Examples:
3802
         *  Map::from( ['a', 'b'] )->shift();
3803
         *  Map::from( [] )->shift();
3804
         *
3805
         * Results:
3806
         * The first example returns "a" and shortens the map to ['b'] only while the
3807
         * second example will return NULL
3808
         *
3809
         * Performance note:
3810
         * The bigger the list, the higher the performance impact because shift()
3811
         * reindexes all existing elements. Usually, it's better to reverse() the list
3812
         * and pop() entries from the list afterwards if a significant number of elements
3813
         * should be removed from the list:
3814
         *
3815
         *  $map->reverse()->pop();
3816
         * instead of
3817
         *  $map->shift( 'a' );
3818
         *
3819
         * @return mixed|null Value from map or null if not found
3820
         */
3821
        public function shift()
3822
        {
3823
                return array_shift( $this->list() );
7✔
3824
        }
3825

3826

3827
        /**
3828
         * Shuffles the elements in the map without returning a new map.
3829
         *
3830
         * Examples:
3831
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle();
3832
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle( true );
3833
         *
3834
         * Results:
3835
         * The map in the first example will contain "a" and "b" in random order and
3836
         * with new keys assigned. The second call will also return all values in
3837
         * random order but preserves the keys of the original list.
3838
         *
3839
         * @param bool $assoc True to preserve keys, false to assign new keys
3840
         * @return self<int|string,mixed> Updated map for fluid interface
3841
         */
3842
        public function shuffle( bool $assoc = false ) : self
3843
        {
3844
                if( $assoc )
14✔
3845
                {
3846
                        $list = $this->list();
7✔
3847
                        $keys = array_keys( $list );
7✔
3848
                        shuffle( $keys );
7✔
3849
                        $items = [];
7✔
3850

3851
                        foreach( $keys as $key ) {
7✔
3852
                                $items[$key] = $list[$key];
7✔
3853
                        }
3854

3855
                        $this->list = $items;
7✔
3856
                }
3857
                else
3858
                {
3859
                        shuffle( $this->list() );
7✔
3860
                }
3861

3862

3863
                return $this;
14✔
3864
        }
3865

3866

3867
        /**
3868
         * Returns a new map with the given number of items skipped.
3869
         *
3870
         * Examples:
3871
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
3872
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
3873
         *      return $item < 4;
3874
         *  } );
3875
         *
3876
         * Results:
3877
         *  [2 => 3, 3 => 4]
3878
         *  [3 => 4]
3879
         *
3880
         * The keys of the items returned in the new map are the same as in the original one.
3881
         *
3882
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
3883
         * @return self<int|string,mixed> New map
3884
         */
3885
        public function skip( $offset ) : self
3886
        {
3887
                if( is_scalar( $offset ) ) {
21✔
3888
                        return new static( array_slice( $this->list(), (int) $offset, null, true ) );
7✔
3889
                }
3890

3891
                if( is_callable( $offset ) )
14✔
3892
                {
3893
                        $idx = 0;
7✔
3894
                        $list = $this->list();
7✔
3895

3896
                        foreach( $list as $key => $item )
7✔
3897
                        {
3898
                                if( !$offset( $item, $key ) ) {
7✔
3899
                                        break;
7✔
3900
                                }
3901

3902
                                ++$idx;
7✔
3903
                        }
3904

3905
                        return new static( array_slice( $list, $idx, null, true ) );
7✔
3906
                }
3907

3908
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as first argument for skip()' );
7✔
3909
        }
3910

3911

3912
        /**
3913
         * Returns a map with the slice from the original map.
3914
         *
3915
         * Examples:
3916
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
3917
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
3918
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
3919
         *
3920
         * Results:
3921
         * The first example will return ['b', 'c'] and the second one ['b'] only.
3922
         * The third example returns ['c'] because the slice starts at the second
3923
         * last value and ends before the last value.
3924
         *
3925
         * The rules for offsets are:
3926
         * - If offset is non-negative, the sequence will start at that offset
3927
         * - If offset is negative, the sequence will start that far from the end
3928
         *
3929
         * Similar for the length:
3930
         * - If length is given and is positive, then the sequence will have up to that many elements in it
3931
         * - If the array is shorter than the length, then only the available array elements will be present
3932
         * - If length is given and is negative then the sequence will stop that many elements from the end
3933
         * - If it is omitted, then the sequence will have everything from offset up until the end
3934
         *
3935
         * The keys of the items returned in the new map are the same as in the original one.
3936
         *
3937
         * @param int $offset Number of elements to start from
3938
         * @param int|null $length Number of elements to return or NULL for no limit
3939
         * @return self<int|string,mixed> New map
3940
         */
3941
        public function slice( int $offset, int $length = null ) : self
3942
        {
3943
                return new static( array_slice( $this->list(), $offset, $length, true ) );
42✔
3944
        }
3945

3946

3947
        /**
3948
         * Tests if at least one element passes the test or is part of the map.
3949
         *
3950
         * Examples:
3951
         *  Map::from( ['a', 'b'] )->some( 'a' );
3952
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
3953
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
3954
         *    return $item === 'a'
3955
         *  } );
3956
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
3957
         *  Map::from( ['1', '2'] )->some( [2], true );
3958
         *
3959
         * Results:
3960
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
3961
         *
3962
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
3963
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3964
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
3965
         */
3966
        public function some( $values, bool $strict = false ) : bool
3967
        {
3968
                $list = $this->list();
42✔
3969

3970
                if( is_iterable( $values ) )
42✔
3971
                {
3972
                        foreach( $values as $entry )
21✔
3973
                        {
3974
                                if( in_array( $entry, $list, $strict ) === true ) {
21✔
3975
                                        return true;
21✔
3976
                                }
3977
                        }
3978

3979
                        return false;
14✔
3980
                }
3981
                elseif( is_callable( $values ) )
28✔
3982
                {
3983
                        foreach( $list as $key => $item )
14✔
3984
                        {
3985
                                if( $values( $item, $key ) ) {
14✔
3986
                                        return true;
14✔
3987
                                }
3988
                        }
3989
                }
3990
                elseif( in_array( $values, $list, $strict ) === true )
21✔
3991
                {
3992
                        return true;
21✔
3993
                }
3994

3995
                return false;
21✔
3996
        }
3997

3998

3999
        /**
4000
         * Sorts all elements using new keys.
4001
         *
4002
         * Examples:
4003
         *  Map::from( ['a' => 1, 'b' => 0] )->sort();
4004
         *  Map::from( [0 => 'b', 1 => 'a'] )->sort();
4005
         *
4006
         * Results:
4007
         *  [0 => 0, 1 => 1]
4008
         *  [0 => 'a', 1 => 'b']
4009
         *
4010
         * The parameter modifies how the values are compared. Possible parameter values are:
4011
         * - SORT_REGULAR : compare elements normally (don't change types)
4012
         * - SORT_NUMERIC : compare elements numerically
4013
         * - SORT_STRING : compare elements as strings
4014
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4015
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4016
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4017
         *
4018
         * The keys aren't preserved and elements get a new index. No new map is created.
4019
         *
4020
         * @param int $options Sort options for sort()
4021
         * @return self<int|string,mixed> Updated map for fluid interface
4022
         */
4023
        public function sort( int $options = SORT_REGULAR ) : self
4024
        {
4025
                sort( $this->list(), $options );
21✔
4026
                return $this;
21✔
4027
        }
4028

4029

4030
        /**
4031
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4032
         *
4033
         * Examples:
4034
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4035
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4036
         *
4037
         * Results:
4038
         * The first example removes all entries after "a", so only ['a'] will be left
4039
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4040
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4041
         * ['a', 'x', 'y', 'c'] afterwards.
4042
         *
4043
         * The rules for offsets are:
4044
         * - If offset is non-negative, the sequence will start at that offset
4045
         * - If offset is negative, the sequence will start that far from the end
4046
         *
4047
         * Similar for the length:
4048
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4049
         * - If the array is shorter than the length, then only the available array elements will be present
4050
         * - If length is given and is negative then the sequence will stop that many elements from the end
4051
         * - If it is omitted, then the sequence will have everything from offset up until the end
4052
         *
4053
         * Numerical array indexes are NOT preserved.
4054
         *
4055
         * @param int $offset Number of elements to start from
4056
         * @param int|null $length Number of elements to remove, NULL for all
4057
         * @param mixed $replacement List of elements to insert
4058
         * @return self<int|string,mixed> New map
4059
         */
4060
        public function splice( int $offset, int $length = null, $replacement = [] ) : self
4061
        {
4062
                if( $length === null ) {
35✔
4063
                        $length = count( $this->list() );
14✔
4064
                }
4065

4066
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
35✔
4067
        }
4068

4069

4070
        /**
4071
         * Returns the strings after the passed value.
4072
         *
4073
         * Examples:
4074
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4075
         *  Map::from( ['abc'] )->strAfter( '' );
4076
         *  Map::from( ['abc'] )->strAfter( 'b' );
4077
         *  Map::from( ['abc'] )->strAfter( 'c' );
4078
         *  Map::from( ['abc'] )->strAfter( 'x' );
4079
         *  Map::from( [''] )->strAfter( '' );
4080
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4081
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4082
         *
4083
         * Results:
4084
         *  ['üß']
4085
         *  ['abc']
4086
         *  ['c']
4087
         *  ['']
4088
         *  []
4089
         *  []
4090
         *  ['1', '1', '1']
4091
         *  ['0', '0']
4092
         *
4093
         * All scalar values (bool, int, float, string) will be converted to strings.
4094
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4095
         *
4096
         * @param string $value Character or string to search for
4097
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4098
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4099
         * @return self<int|string,mixed> New map
4100
         */
4101
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4102
        {
4103
                $list = [];
7✔
4104
                $len = mb_strlen( $value );
7✔
4105
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
7✔
4106

4107
                foreach( $this->list() as $key => $entry )
7✔
4108
                {
4109
                        if( is_scalar( $entry ) )
7✔
4110
                        {
4111
                                $pos = null;
7✔
4112
                                $str = (string) $entry;
7✔
4113

4114
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
7✔
4115
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
7✔
4116
                                } elseif( $str !== '' && $pos !== false ) {
7✔
4117
                                        $list[$key] = $str;
7✔
4118
                                }
4119
                        }
4120
                }
4121

4122
                return new static( $list );
7✔
4123
        }
4124

4125

4126
        /**
4127
         * Returns the strings before the passed value.
4128
         *
4129
         * Examples:
4130
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4131
         *  Map::from( ['abc'] )->strBefore( '' );
4132
         *  Map::from( ['abc'] )->strBefore( 'b' );
4133
         *  Map::from( ['abc'] )->strBefore( 'a' );
4134
         *  Map::from( ['abc'] )->strBefore( 'x' );
4135
         *  Map::from( [''] )->strBefore( '' );
4136
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4137
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4138
         *
4139
         * Results:
4140
         *  ['äö']
4141
         *  ['abc']
4142
         *  ['a']
4143
         *  ['']
4144
         *  []
4145
         *  []
4146
         *  ['1', '1', '1']
4147
         *  ['0', '0']
4148
         *
4149
         * All scalar values (bool, int, float, string) will be converted to strings.
4150
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4151
         *
4152
         * @param string $value Character or string to search for
4153
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4154
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4155
         * @return self<int|string,mixed> New map
4156
         */
4157
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4158
        {
4159
                $list = [];
7✔
4160
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
7✔
4161

4162
                foreach( $this->list() as $key => $entry )
7✔
4163
                {
4164
                        if( is_scalar( $entry ) )
7✔
4165
                        {
4166
                                $pos = null;
7✔
4167
                                $str = (string) $entry;
7✔
4168

4169
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
7✔
4170
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
7✔
4171
                                } elseif( $str !== '' && $pos !== false ) {
7✔
4172
                                        $list[$key] = $str;
7✔
4173
                                } else {
1✔
4174
                                }
4175
                        }
4176
                }
4177

4178
                return new static( $list );
7✔
4179
        }
4180

4181

4182
        /**
4183
         * Tests if at least one of the passed strings is part of at least one entry.
4184
         *
4185
         * Examples:
4186
         *  Map::from( ['abc'] )->strContains( '' );
4187
         *  Map::from( ['abc'] )->strContains( 'a' );
4188
         *  Map::from( ['abc'] )->strContains( 'bc' );
4189
         *  Map::from( [12345] )->strContains( '23' );
4190
         *  Map::from( [123.4] )->strContains( 23.4 );
4191
         *  Map::from( [12345] )->strContains( false );
4192
         *  Map::from( [12345] )->strContains( true );
4193
         *  Map::from( [false] )->strContains( false );
4194
         *  Map::from( [''] )->strContains( false );
4195
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4196
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4197
         *
4198
         *  Map::from( ['abc'] )->strContains( 'd' );
4199
         *  Map::from( ['abc'] )->strContains( 'cb' );
4200
         *  Map::from( [23456] )->strContains( true );
4201
         *  Map::from( [false] )->strContains( 0 );
4202
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4203
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4204
         *
4205
         * Results:
4206
         * The first eleven examples will return TRUE while the last six will return FALSE.
4207
         *
4208
         * @param array|string $value The string or list of strings to search for in each entry
4209
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4210
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4211
         */
4212
        public function strContains( $value, string $encoding = 'UTF-8' ) : bool
4213
        {
4214
                foreach( $this->list() as $entry )
7✔
4215
                {
4216
                        $entry = (string) $entry;
7✔
4217

4218
                        foreach( (array) $value as $str )
7✔
4219
                        {
4220
                                $str = (string) $str;
7✔
4221

4222
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
7✔
4223
                                        return true;
7✔
4224
                                }
4225
                        }
4226
                }
4227

4228
                return false;
7✔
4229
        }
4230

4231

4232
        /**
4233
         * Tests if all of the entries contains one of the passed strings.
4234
         *
4235
         * Examples:
4236
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
4237
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
4238
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
4239
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
4240
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
4241
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
4242
         *  Map::from( ['', false] )->strContainsAll( false );
4243
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
4244
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
4245
         *
4246
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
4247
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
4248
         *  Map::from( [23456, '123'] )->strContainsAll( true );
4249
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
4250
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
4251
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
4252
         *
4253
         * Results:
4254
         * The first nine examples will return TRUE while the last six will return FALSE.
4255
         *
4256
         * @param array|string $value The string or list of strings to search for in each entry
4257
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4258
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
4259
         */
4260
        public function strContainsAll( $value, string $encoding = 'UTF-8' ) : bool
4261
        {
4262
                $list = [];
7✔
4263

4264
                foreach( $this->list() as $entry )
7✔
4265
                {
4266
                        $entry = (string) $entry;
7✔
4267
                        $list[$entry] = 0;
7✔
4268

4269
                        foreach( (array) $value as $str )
7✔
4270
                        {
4271
                                $str = (string) $str;
7✔
4272

4273
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
7✔
4274
                                        $list[$entry] = 1; break;
7✔
4275
                                }
4276
                        }
4277
                }
4278

4279
                return array_sum( $list ) === count( $list );
7✔
4280
        }
4281

4282

4283
        /**
4284
         * Tests if at least one of the entries ends with one of the passed strings.
4285
         *
4286
         * Examples:
4287
         *  Map::from( ['abc'] )->strEnds( '' );
4288
         *  Map::from( ['abc'] )->strEnds( 'c' );
4289
         *  Map::from( ['abc'] )->strEnds( 'bc' );
4290
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
4291
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
4292
         *  Map::from( ['abc'] )->strEnds( 'a' );
4293
         *  Map::from( ['abc'] )->strEnds( 'cb' );
4294
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
4295
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
4296
         *
4297
         * Results:
4298
         * The first five examples will return TRUE while the last four will return FALSE.
4299
         *
4300
         * @param array|string $value The string or strings to search for in each entry
4301
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4302
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
4303
         */
4304
        public function strEnds( $value, string $encoding = 'UTF-8' ) : bool
4305
        {
4306
                foreach( $this->list() as $entry )
7✔
4307
                {
4308
                        $entry = (string) $entry;
7✔
4309

4310
                        foreach( (array) $value as $str )
7✔
4311
                        {
4312
                                $len = mb_strlen( (string) $str );
7✔
4313

4314
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
7✔
4315
                                        return true;
7✔
4316
                                }
4317
                        }
4318
                }
4319

4320
                return false;
7✔
4321
        }
4322

4323

4324
        /**
4325
         * Tests if all of the entries ends with at least one of the passed strings.
4326
         *
4327
         * Examples:
4328
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
4329
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
4330
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
4331
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
4332
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
4333
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
4334
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
4335
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
4336
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
4337
         *
4338
         * Results:
4339
         * The first five examples will return TRUE while the last four will return FALSE.
4340
         *
4341
         * @param array|string $value The string or strings to search for in each entry
4342
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4343
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4344
         */
4345
        public function strEndsAll( $value, string $encoding = 'UTF-8' ) : bool
4346
        {
4347
                $list = [];
7✔
4348

4349
                foreach( $this->list() as $entry )
7✔
4350
                {
4351
                        $entry = (string) $entry;
7✔
4352
                        $list[$entry] = 0;
7✔
4353

4354
                        foreach( (array) $value as $str )
7✔
4355
                        {
4356
                                $len = mb_strlen( (string) $str );
7✔
4357

4358
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
7✔
4359
                                        $list[$entry] = 1; break;
7✔
4360
                                }
4361
                        }
4362
                }
4363

4364
                return array_sum( $list ) === count( $list );
7✔
4365
        }
4366

4367

4368
        /**
4369
         * Returns an element by key and casts it to string if possible.
4370
         *
4371
         * Examples:
4372
         *  Map::from( ['a' => true] )->string( 'a' );
4373
         *  Map::from( ['a' => 1] )->string( 'a' );
4374
         *  Map::from( ['a' => 1.1] )->string( 'a' );
4375
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
4376
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
4377
         *  Map::from( [] )->string( 'a', function() { return 'no'; } );
4378
         *
4379
         *  Map::from( [] )->string( 'b' );
4380
         *  Map::from( ['b' => ''] )->string( 'b' );
4381
         *  Map::from( ['b' => null] )->string( 'b' );
4382
         *  Map::from( ['b' => [true]] )->string( 'b' );
4383
         *  Map::from( ['b' => resource] )->string( 'b' );
4384
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
4385
         *
4386
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
4387
         *
4388
         * Results:
4389
         * The first six examples will return the value as string while the 9th to 12th
4390
         * example returns an empty string. The last example will throw an exception.
4391
         *
4392
         * This does also work for multi-dimensional arrays by passing the keys
4393
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
4394
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
4395
         * public properties of objects or objects implementing __isset() and __get() methods.
4396
         *
4397
         * @param int|string $key Key or path to the requested item
4398
         * @param mixed $default Default value if key isn't found (will be casted to bool)
4399
         * @return string Value from map or default value
4400
         */
4401
        public function string( $key, $default = '' ) : string
4402
        {
4403
                return (string) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
21✔
4404
        }
4405

4406

4407
        /**
4408
         * Converts all alphabetic characters in strings to lower case.
4409
         *
4410
         * Examples:
4411
         *  Map::from( ['My String'] )->strLower();
4412
         *  Map::from( ['Τάχιστη'] )->strLower();
4413
         *  Map::from( ['Äpfel', 'Birnen'] )->strLower( 'ISO-8859-1' );
4414
         *  Map::from( [123] )->strLower();
4415
         *  Map::from( [new stdClass] )->strLower();
4416
         *
4417
         * Results:
4418
         * The first example will return ["my string"], the second one ["τάχιστη"] and
4419
         * the third one ["äpfel", "birnen"]. The last two strings will be unchanged.
4420
         *
4421
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4422
         * @return self<int|string,mixed> Updated map for fluid interface
4423
         */
4424
        public function strLower( string $encoding = 'UTF-8' ) : self
4425
        {
4426
                foreach( $this->list() as &$entry )
7✔
4427
                {
4428
                        if( is_string( $entry ) ) {
7✔
4429
                                $entry = mb_strtolower( $entry, $encoding );
7✔
4430
                        }
4431
                }
4432

4433
                return $this;
7✔
4434
        }
4435

4436

4437
        /**
4438
         * Replaces all occurrences of the search string with the replacement string.
4439
         *
4440
         * Examples:
4441
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
4442
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
4443
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
4444
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
4445
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
4446
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
4447
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
4448
         *
4449
         * Restults:
4450
         * ['google.de', 'aimeos.de']
4451
         * ['google.de', 'aimeos.de']
4452
         * ['google.de', 'aimeos']
4453
         * ['google.fr', 'aimeos.de']
4454
         * ['google.de', 'aimeos.de']
4455
         * ['google.de', 'aimeos.de', 123]
4456
         * ['GOOGLE.de', 'AIMEOS.de']
4457
         *
4458
         * If you use an array of strings for search or search/replacement, the order of
4459
         * the strings matters! Each search string found is replaced by the corresponding
4460
         * replacement string at the same position.
4461
         *
4462
         * In case of array parameters and if the number of replacement strings is less
4463
         * than the number of search strings, the search strings with no corresponding
4464
         * replacement string are replaced with empty strings. Replacement strings with
4465
         * no corresponding search string are ignored.
4466
         *
4467
         * An array parameter for the replacements is only allowed if the search parameter
4468
         * is an array of strings too!
4469
         *
4470
         * Because the method replaces from left to right, it might replace a previously
4471
         * inserted value when doing multiple replacements. Entries which are non-string
4472
         * values are left untouched.
4473
         *
4474
         * @param array|string $search String or list of strings to search for
4475
         * @param array|string $replace String or list of strings of replacement strings
4476
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
4477
         * @return self<int|string,mixed> Updated map for fluid interface
4478
         */
4479
        public function strReplace( $search, $replace, bool $case = false ) : self
4480
        {
4481
                $fcn = $case ? 'str_ireplace' : 'str_replace';
7✔
4482

4483
                foreach( $this->list() as &$entry )
7✔
4484
                {
4485
                        if( is_string( $entry ) ) {
7✔
4486
                                $entry = $fcn( $search, $replace, $entry );
7✔
4487
                        }
4488
                }
4489

4490
                return $this;
7✔
4491
        }
4492

4493

4494
        /**
4495
         * Tests if at least one of the entries starts with at least one of the passed strings.
4496
         *
4497
         * Examples:
4498
         *  Map::from( ['abc'] )->strStarts( '' );
4499
         *  Map::from( ['abc'] )->strStarts( 'a' );
4500
         *  Map::from( ['abc'] )->strStarts( 'ab' );
4501
         *  Map::from( ['abc'] )->strStarts( ['a', 'b'] );
4502
         *  Map::from( ['abc'] )->strStarts( 'ab', 'ASCII' );
4503
         *  Map::from( ['abc'] )->strStarts( 'b' );
4504
         *  Map::from( ['abc'] )->strStarts( 'bc' );
4505
         *  Map::from( ['abc'] )->strStarts( ['b', 'c'] );
4506
         *  Map::from( ['abc'] )->strStarts( 'bc', 'ASCII' );
4507
         *
4508
         * Results:
4509
         * The first five examples will return TRUE while the last four will return FALSE.
4510
         *
4511
         * @param array|string $value The string or strings to search for in each entry
4512
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4513
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4514
         */
4515
        public function strStarts( $value, string $encoding = 'UTF-8' ) : bool
4516
        {
4517
                foreach( $this->list() as $entry )
7✔
4518
                {
4519
                        $entry = (string) $entry;
7✔
4520

4521
                        foreach( (array) $value as $str )
7✔
4522
                        {
4523
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
7✔
4524
                                        return true;
7✔
4525
                                }
4526
                        }
4527
                }
4528

4529
                return false;
7✔
4530
        }
4531

4532

4533
        /**
4534
         * Tests if all of the entries starts with one of the passed strings.
4535
         *
4536
         * Examples:
4537
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
4538
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
4539
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
4540
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
4541
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
4542
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
4543
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
4544
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
4545
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
4546
         *
4547
         * Results:
4548
         * The first five examples will return TRUE while the last four will return FALSE.
4549
         *
4550
         * @param array|string $value The string or strings to search for in each entry
4551
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4552
         * @return bool TRUE if one of the entries starts with one of the strings, FALSE if not
4553
         */
4554
        public function strStartsAll( $value, string $encoding = 'UTF-8' ) : bool
4555
        {
4556
                $list = [];
7✔
4557

4558
                foreach( $this->list() as $entry )
7✔
4559
                {
4560
                        $entry = (string) $entry;
7✔
4561
                        $list[$entry] = 0;
7✔
4562

4563
                        foreach( (array) $value as $str )
7✔
4564
                        {
4565
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
7✔
4566
                                        $list[$entry] = 1; break;
7✔
4567
                                }
4568
                        }
4569
                }
4570

4571
                return array_sum( $list ) === count( $list );
7✔
4572
        }
4573

4574

4575
        /**
4576
         * Converts all alphabetic characters in strings to upper case.
4577
         *
4578
         * Examples:
4579
         *  Map::from( ['My String'] )->strUpper();
4580
         *  Map::from( ['τάχιστη'] )->strUpper();
4581
         *  Map::from( ['äpfel', 'birnen'] )->strUpper( 'ISO-8859-1' );
4582
         *  Map::from( [123] )->strUpper();
4583
         *  Map::from( [new stdClass] )->strUpper();
4584
         *
4585
         * Results:
4586
         * The first example will return ["MY STRING"], the second one ["ΤΆΧΙΣΤΗ"] and
4587
         * the third one ["ÄPFEL", "BIRNEN"]. The last two strings will be unchanged.
4588
         *
4589
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4590
         * @return self<int|string,mixed> Updated map for fluid interface
4591
         */
4592
        public function strUpper( string $encoding = 'UTF-8' ) :self
4593
        {
4594
                foreach( $this->list() as &$entry )
7✔
4595
                {
4596
                        if( is_string( $entry ) ) {
7✔
4597
                                $entry = mb_strtoupper( $entry, $encoding );
7✔
4598
                        }
4599
                }
4600

4601
                return $this;
7✔
4602
        }
4603

4604

4605
        /**
4606
         * Adds a suffix at the end of each map entry.
4607
         *
4608
         * By defaul, nested arrays are walked recusively so all entries at all levels are suffixed.
4609
         *
4610
         * Examples:
4611
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
4612
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
4613
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
4614
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
4615
         *      return '-' . ( ord( $item ) + ord( $key ) );
4616
         *  } );
4617
         *
4618
         * Results:
4619
         *  The first example returns ['a-1', 'b-1'] while the second one will return
4620
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
4621
         *  level only so it will return ['a-1', ['b']]. The forth example passing
4622
         *  the closure will return ['a-145', 'b-147'].
4623
         *
4624
         * The keys are preserved using this method.
4625
         *
4626
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
4627
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
4628
         * @return self<int|string,mixed> Updated map for fluid interface
4629
         */
4630
        public function suffix( $suffix, int $depth = null ) : self
4631
        {
4632
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
5✔
4633

4634
                        foreach( $list as $key => $item )
7✔
4635
                        {
4636
                                if( is_array( $item ) ) {
7✔
4637
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
7✔
4638
                                } else {
4639
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
7✔
4640
                                }
4641
                        }
4642

4643
                        return $list;
7✔
4644
                };
7✔
4645

4646
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
7✔
4647
                return $this;
7✔
4648
        }
4649

4650

4651
        /**
4652
         * Returns the sum of all integer and float values in the map.
4653
         *
4654
         * Examples:
4655
         *  Map::from( [1, 3, 5] )->sum();
4656
         *  Map::from( [1, 'sum', 5] )->sum();
4657
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
4658
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
4659
         *
4660
         * Results:
4661
         * The first line will return "9", the second one "6", the third one "90"
4662
         * and the last one "80".
4663
         *
4664
         * This does also work for multi-dimensional arrays by passing the keys
4665
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
4666
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
4667
         * public properties of objects or objects implementing __isset() and __get() methods.
4668
         *
4669
         * @param string|null $key Key or path to the values in the nested array or object to sum up
4670
         * @return float Sum of all elements or 0 if there are no elements in the map
4671
         */
4672
        public function sum( string $key = null ) : float
4673
        {
4674
                $vals = $key !== null ? $this->col( $key )->toArray() : $this->list();
28✔
4675
                return !empty( $vals ) ? array_sum( $vals ) : 0;
28✔
4676
        }
4677

4678

4679
        /**
4680
         * Returns a new map with the given number of items.
4681
         *
4682
         * The keys of the items returned in the new map are the same as in the original one.
4683
         *
4684
         * Examples:
4685
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
4686
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
4687
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
4688
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
4689
         *      return $item < 2;
4690
         *  } );
4691
         *
4692
         * Results:
4693
         *  [0 => 1, 1 => 2]
4694
         *  [1 => 2, 2 => 3]
4695
         *  [2 => 3, 3 => 4]
4696
         *  [1 => 2, 2 => 3]
4697
         *
4698
         * The keys of the items returned in the new map are the same as in the original one.
4699
         *
4700
         * @param int $size Number of items to return
4701
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4702
         * @return self<int|string,mixed> New map
4703
         */
4704
        public function take( int $size, $offset = 0 ) : self
4705
        {
4706
                $list = $this->list();
35✔
4707

4708
                if( is_scalar( $offset ) ) {
35✔
4709
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
21✔
4710
                }
4711

4712
                if( is_callable( $offset ) )
14✔
4713
                {
4714
                        $idx = 0;
7✔
4715

4716
                        foreach( $list as $key => $item )
7✔
4717
                        {
4718
                                if( !$offset( $item, $key ) ) {
7✔
4719
                                        break;
7✔
4720
                                }
4721

4722
                                ++$idx;
7✔
4723
                        }
4724

4725
                        return new static( array_slice( $list, $idx, $size, true ) );
7✔
4726
                }
4727

4728
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as second argument for take()' );
7✔
4729
        }
4730

4731

4732
        /**
4733
         * Passes a clone of the map to the given callback.
4734
         *
4735
         * Use it to "tap" into a chain of methods to check the state between two
4736
         * method calls. The original map is not altered by anything done in the
4737
         * callback.
4738
         *
4739
         * Examples:
4740
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
4741
         *    print_r( $map->remove( 0 )->toArray() );
4742
         *  } )->first();
4743
         *
4744
         * Results:
4745
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
4746
         * then prints the items without the first (`[2, 3]`) in the function passed
4747
         * to `tap()` and returns the first item ("1") at the end.
4748
         *
4749
         * @param callable $callback Function receiving ($map) parameter
4750
         * @return self<int|string,mixed> Same map for fluid interface
4751
         */
4752
        public function tap( callable $callback ) : self
4753
        {
4754
                $callback( clone $this );
7✔
4755
                return $this;
7✔
4756
        }
4757

4758

4759
        /**
4760
         * Returns the elements as a plain array.
4761
         *
4762
         * @return array<int|string,mixed> Plain array
4763
         */
4764
        public function toArray() : array
4765
        {
4766
                return $this->list = $this->array( $this->list );
1,442✔
4767
        }
4768

4769

4770
        /**
4771
         * Returns the elements encoded as JSON string.
4772
         *
4773
         * There are several options available to modify the JSON output:
4774
         * {@link https://www.php.net/manual/en/function.json-encode.php}
4775
         * The parameter can be a single JSON_* constant or a bitmask of several
4776
         * constants combine by bitwise OR (|), e.g.:
4777
         *
4778
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
4779
         *
4780
         * @param int $options Combination of JSON_* constants
4781
         * @return string|null Array encoded as JSON string or NULL on failure
4782
         */
4783
        public function toJson( int $options = 0 ) : ?string
4784
        {
4785
                $result = json_encode( $this->list(), $options );
14✔
4786
                return $result !== false ? $result : null;
14✔
4787
        }
4788

4789

4790
        /**
4791
         * Creates a HTTP query string from the map elements.
4792
         *
4793
         * Examples:
4794
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
4795
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
4796
         *
4797
         * Results:
4798
         *  a=1&b=2
4799
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
4800
         *
4801
         * @return string Parameter string for GET requests
4802
         */
4803
        public function toUrl() : string
4804
        {
4805
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
14✔
4806
        }
4807

4808

4809
        /**
4810
         * Exchanges rows and columns for a two dimensional map.
4811
         *
4812
         * Examples:
4813
         *  Map::from( [
4814
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
4815
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
4816
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
4817
         *  ] )->transpose();
4818
         *
4819
         *  Map::from( [
4820
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
4821
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
4822
         *    ['name' => 'C', 2020 => 400]
4823
         *  ] );
4824
         *
4825
         * Results:
4826
         *  [
4827
         *    'name' => ['A', 'B', 'C'],
4828
         *    2020 => [200, 300, 400],
4829
         *    2021 => [100, 200, 300],
4830
         *    2022 => [50, 100, 200]
4831
         *  ]
4832
         *
4833
         *  [
4834
         *    'name' => ['A', 'B', 'C'],
4835
         *    2020 => [200, 300, 400],
4836
         *    2021 => [100, 200],
4837
         *    2022 => [50]
4838
         *  ]
4839
         *
4840
         * @return self<int|string,mixed> New map
4841
         */
4842
        public function transpose() : self
4843
        {
4844
                $result = [];
14✔
4845

4846
                foreach( (array) $this->first( [] ) as $key => $col ) {
14✔
4847
                        $result[$key] = array_column( $this->list(), $key );
14✔
4848
                }
4849

4850
                return new static( $result );
14✔
4851
        }
4852

4853

4854
        /**
4855
         * Traverses trees of nested items passing each item to the callback.
4856
         *
4857
         * This does work for nested arrays and objects with public properties or
4858
         * objects implementing __isset() and __get() methods. To build trees
4859
         * of nested items, use the tree() method.
4860
         *
4861
         * Examples:
4862
         *   Map::from( [[
4863
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
4864
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
4865
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
4866
         *     ]
4867
         *   ]] )->traverse();
4868
         *
4869
         *   Map::from( [[
4870
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
4871
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
4872
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
4873
         *     ]
4874
         *   ]] )->traverse( function( $entry, $key, $level ) {
4875
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
4876
         *   } );
4877
         *
4878
         *   Map::from( [[
4879
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
4880
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
4881
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
4882
         *     ]
4883
         *   ]] )->traverse( function( $entry, $key, $level ) {
4884
         *     return !isset( $entry['children'] ) ? $entry : null;
4885
         *   } )->filter();
4886
         *
4887
         *   Map::from( [[
4888
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
4889
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
4890
         *     ]
4891
         *   ]] )->traverse( null, 'nodes' );
4892
         *
4893
         * Results:
4894
         *   [
4895
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
4896
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
4897
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
4898
         *   ]
4899
         *
4900
         *   ['- n1', '-- n2', '-- n3']
4901
         *
4902
         *   [
4903
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
4904
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
4905
         *   ]
4906
         *
4907
         *   [
4908
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
4909
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
4910
         *   ]
4911
         *
4912
         * @param \Closure|null $callback Callback with (entry, key, level) arguments, returns the entry added to result
4913
         * @param string $nestKey Key to the children of each item
4914
         * @return self<int|string,mixed> New map with all items as flat list
4915
         */
4916
        public function traverse( \Closure $callback = null, string $nestKey = 'children' ) : self
4917
        {
4918
                $result = [];
28✔
4919
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
28✔
4920

4921
                return map( $result );
28✔
4922
        }
4923

4924

4925
        /**
4926
         * Creates a tree structure from the list items.
4927
         *
4928
         * Use this method to rebuild trees e.g. from database records. To traverse
4929
         * trees, use the traverse() method.
4930
         *
4931
         * Examples:
4932
         *  Map::from( [
4933
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
4934
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
4935
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
4936
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
4937
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
4938
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
4939
         *  ] )->tree( 'id', 'pid' );
4940
         *
4941
         * Results:
4942
         *   [1 => [
4943
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
4944
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
4945
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
4946
         *       ]],
4947
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
4948
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
4949
         *       ]],
4950
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
4951
         *     ]
4952
         *   ]]
4953
         *
4954
         * To build the tree correctly, the items must be in order or at least the
4955
         * nodes of the lower levels must come first. For a tree like this:
4956
         * n1
4957
         * |- n2
4958
         * |  |- n3
4959
         * |- n4
4960
         * |  |- n5
4961
         * |- n6
4962
         *
4963
         * Accepted item order:
4964
         * - in order: n1, n2, n3, n4, n5, n6
4965
         * - lower levels first: n1, n2, n4, n6, n3, n5
4966
         *
4967
         * If your items are unordered, apply usort() first to the map entries, e.g.
4968
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
4969
         *     return $item1['lvl'] <=> $item2['lvl'];
4970
         *   } );
4971
         *
4972
         * @param string $idKey Name of the key with the unique ID of the node
4973
         * @param string $parentKey Name of the key with the ID of the parent node
4974
         * @param string $nestKey Name of the key with will contain the children of the node
4975
         * @return self<int|string,mixed> New map with one or more root tree nodes
4976
         */
4977
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
4978
        {
4979
                $this->list();
7✔
4980
                $trees = $refs = [];
7✔
4981

4982
                foreach( $this->list as &$node )
7✔
4983
                {
4984
                        $node[$nestKey] = [];
7✔
4985
                        $refs[$node[$idKey]] = &$node;
7✔
4986

4987
                        if( $node[$parentKey] ) {
7✔
4988
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
7✔
4989
                        } else {
4990
                                $trees[$node[$idKey]] = &$node;
7✔
4991
                        }
4992
                }
4993

4994
                return map( $trees );
7✔
4995
        }
4996

4997

4998
        /**
4999
         * Removes the passed characters from the left/right of all strings.
5000
         *
5001
         * Examples:
5002
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5003
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5004
         *
5005
         * Results:
5006
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5007
         *
5008
         * @param string $chars List of characters to trim
5009
         * @return self<int|string,mixed> Updated map for fluid interface
5010
         */
5011
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5012
        {
5013
                foreach( $this->list() as &$entry )
7✔
5014
                {
5015
                        if( is_string( $entry ) ) {
7✔
5016
                                $entry = trim( $entry, $chars );
7✔
5017
                        }
5018
                }
5019

5020
                return $this;
7✔
5021
        }
5022

5023

5024
        /**
5025
         * Sorts all elements using a callback and maintains the key association.
5026
         *
5027
         * The given callback will be used to compare the values. The callback must accept
5028
         * two parameters (item A and B) and must return -1 if item A is smaller than
5029
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5030
         * method name and an anonymous function can be passed.
5031
         *
5032
         * Examples:
5033
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( 'strcasecmp' );
5034
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( function( $itemA, $itemB ) {
5035
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5036
         *  } );
5037
         *
5038
         * Results:
5039
         *  ['b' => 'a', 'a' => 'B']
5040
         *  ['b' => 'a', 'a' => 'B']
5041
         *
5042
         * The keys are preserved using this method and no new map is created.
5043
         *
5044
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5045
         * @return self<int|string,mixed> Updated map for fluid interface
5046
         */
5047
        public function uasort( callable $callback ) : self
5048
        {
5049
                uasort( $this->list(), $callback );
7✔
5050
                return $this;
7✔
5051
        }
5052

5053

5054
        /**
5055
         * Sorts the map elements by their keys using a callback.
5056
         *
5057
         * The given callback will be used to compare the keys. The callback must accept
5058
         * two parameters (key A and B) and must return -1 if key A is smaller than
5059
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5060
         * method name and an anonymous function can be passed.
5061
         *
5062
         * Examples:
5063
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( 'strcasecmp' );
5064
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( function( $keyA, $keyB ) {
5065
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5066
         *  } );
5067
         *
5068
         * Results:
5069
         *  ['a' => 'b', 'B' => 'a']
5070
         *  ['a' => 'b', 'B' => 'a']
5071
         *
5072
         * The keys are preserved using this method and no new map is created.
5073
         *
5074
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5075
         * @return self<int|string,mixed> Updated map for fluid interface
5076
         */
5077
        public function uksort( callable $callback ) : self
5078
        {
5079
                uksort( $this->list(), $callback );
7✔
5080
                return $this;
7✔
5081
        }
5082

5083

5084
        /**
5085
         * Builds a union of the elements and the given elements without overwriting existing ones.
5086
         * Existing keys in the map will not be overwritten
5087
         *
5088
         * Examples:
5089
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
5090
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
5091
         *
5092
         * Results:
5093
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
5094
         * isn't overwritten. In the second example, the result will be a combined
5095
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
5096
         *
5097
         * If list entries should be overwritten,  please use merge() instead!
5098
         * The keys are preserved using this method and no new map is created.
5099
         *
5100
         * @param iterable<int|string,mixed> $elements List of elements
5101
         * @return self<int|string,mixed> Updated map for fluid interface
5102
         */
5103
        public function union( iterable $elements ) : self
5104
        {
5105
                $this->list = $this->list() + $this->array( $elements );
14✔
5106
                return $this;
14✔
5107
        }
5108

5109

5110
        /**
5111
         * Returns only unique elements from the map incl. their keys.
5112
         *
5113
         * Examples:
5114
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
5115
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
5116
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
5117
         *
5118
         * Results:
5119
         * [0 => 'a', 1 => 'b', 3 => 'c']
5120
         * [['p' => 1], ['p' => 2]]
5121
         * [['i' => ['p' => '1']]]
5122
         *
5123
         * Two elements are considered equal if comparing their string representions returns TRUE:
5124
         * (string) $elem1 === (string) $elem2
5125
         *
5126
         * The keys of the elements are only preserved in the new map if no key is passed.
5127
         *
5128
         * @param string|null $key Key or path of the nested array or object to check for
5129
         * @return self<int|string,mixed> New map
5130
         */
5131
        public function unique( string $key = null ) : self
5132
        {
5133
                if( $key !== null ) {
28✔
5134
                        return $this->col( null, $key )->values();
14✔
5135
                }
5136

5137
                return new static( array_unique( $this->list() ) );
14✔
5138
        }
5139

5140

5141
        /**
5142
         * Pushes an element onto the beginning of the map without returning a new map.
5143
         *
5144
         * Examples:
5145
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
5146
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
5147
         *
5148
         * Results:
5149
         *  ['d', 'a', 'b']
5150
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
5151
         *
5152
         * The keys of the elements are only preserved in the new map if no key is passed.
5153
         *
5154
         * Performance note:
5155
         * The bigger the list, the higher the performance impact because unshift()
5156
         * needs to create a new list and copies all existing elements to the new
5157
         * array. Usually, it's better to push() new entries at the end and reverse()
5158
         * the list afterwards:
5159
         *
5160
         *  $map->push( 'a' )->push( 'b' )->reverse();
5161
         * instead of
5162
         *  $map->unshift( 'a' )->unshift( 'b' );
5163
         *
5164
         * @param mixed $value Item to add at the beginning
5165
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
5166
         * @return self<int|string,mixed> Updated map for fluid interface
5167
         */
5168
        public function unshift( $value, $key = null ) : self
5169
        {
5170
                if( $key === null ) {
21✔
5171
                        array_unshift( $this->list(), $value );
14✔
5172
                } else {
5173
                        $this->list = [$key => $value] + $this->list();
7✔
5174
                }
5175

5176
                return $this;
21✔
5177
        }
5178

5179

5180
        /**
5181
         * Sorts all elements using a callback using new keys.
5182
         *
5183
         * The given callback will be used to compare the values. The callback must accept
5184
         * two parameters (item A and B) and must return -1 if item A is smaller than
5185
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5186
         * method name and an anonymous function can be passed.
5187
         *
5188
         * Examples:
5189
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( 'strcasecmp' );
5190
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( function( $itemA, $itemB ) {
5191
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5192
         *  } );
5193
         *
5194
         * Results:
5195
         *  [0 => 'a', 1 => 'B']
5196
         *  [0 => 'a', 1 => 'B']
5197
         *
5198
         * The keys aren't preserved and elements get a new index. No new map is created.
5199
         *
5200
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5201
         * @return self<int|string,mixed> Updated map for fluid interface
5202
         */
5203
        public function usort( callable $callback ) : self
5204
        {
5205
                usort( $this->list(), $callback );
7✔
5206
                return $this;
7✔
5207
        }
5208

5209

5210
        /**
5211
         * Resets the keys and return the values in a new map.
5212
         *
5213
         * Examples:
5214
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
5215
         *
5216
         * Results:
5217
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
5218
         *
5219
         * @return self<int|string,mixed> New map of the values
5220
         */
5221
        public function values() : self
5222
        {
5223
                return new static( array_values( $this->list() ) );
84✔
5224
        }
5225

5226

5227
        /**
5228
         * Applies the given callback to all elements.
5229
         *
5230
         * To change the values of the Map, specify the value parameter as reference
5231
         * (&$value). You can only change the values but not the keys nor the array
5232
         * structure.
5233
         *
5234
         * Examples:
5235
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
5236
         *    $value = strtoupper( $value );
5237
         *  } );
5238
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
5239
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
5240
         *  } );
5241
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
5242
         *    $value = $data[$value] ?? $value;
5243
         *  }, [1 => 'one', 2 => 'two'] );
5244
         *
5245
         * Results:
5246
         * The first example will change the Map elements to:
5247
         *   ['A', 'B', ['C', 'D'], 'E']
5248
         * The output of the second one will be:
5249
         *  ASCII 66 is B
5250
         *  ASCII 97 is a
5251
         * The last example changes the Map elements to:
5252
         *  ['one', 'two', 3]
5253
         *
5254
         * By default, Map elements which are arrays will be traversed recursively.
5255
         * To iterate over the Map elements only, pass FALSE as third parameter.
5256
         *
5257
         * @param callable $callback Function with (item, key, data) parameters
5258
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
5259
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
5260
         * @return self<int|string,mixed> Updated map for fluid interface
5261
         */
5262
        public function walk( callable $callback, $data = null, bool $recursive = true ) : self
5263
        {
5264
                if( $recursive ) {
21✔
5265
                        array_walk_recursive( $this->list(), $callback, $data );
14✔
5266
                } else {
5267
                        array_walk( $this->list(), $callback, $data );
7✔
5268
                }
5269

5270
                return $this;
21✔
5271
        }
5272

5273

5274
        /**
5275
         * Filters the list of elements by a given condition.
5276
         *
5277
         * Examples:
5278
         *  Map::from( [
5279
         *    ['id' => 1, 'type' => 'name'],
5280
         *    ['id' => 2, 'type' => 'short'],
5281
         *  ] )->where( 'type', '==', 'name' );
5282
         *
5283
         *  Map::from( [
5284
         *    ['id' => 3, 'price' => 10],
5285
         *    ['id' => 4, 'price' => 50],
5286
         *  ] )->where( 'price', '>', 20 );
5287
         *
5288
         *  Map::from( [
5289
         *    ['id' => 3, 'price' => 10],
5290
         *    ['id' => 4, 'price' => 50],
5291
         *  ] )->where( 'price', 'in', [10, 25] );
5292
         *
5293
         *  Map::from( [
5294
         *    ['id' => 3, 'price' => 10],
5295
         *    ['id' => 4, 'price' => 50],
5296
         *  ] )->where( 'price', '-', [10, 100] );
5297
         *
5298
         *  Map::from( [
5299
         *    ['item' => ['id' => 3, 'price' => 10]],
5300
         *    ['item' => ['id' => 4, 'price' => 50]],
5301
         *  ] )->where( 'item/price', '>', 30 );
5302
         *
5303
         * Results:
5304
         *  [0 => ['id' => 1, 'type' => 'name']]
5305
         *  [1 => ['id' => 4, 'price' => 50]]
5306
         *  [0 => ['id' => 3, 'price' => 10]]
5307
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
5308
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
5309
         *
5310
         * Available operators are:
5311
         * * '==' : Equal
5312
         * * '===' : Equal and same type
5313
         * * '!=' : Not equal
5314
         * * '!==' : Not equal and same type
5315
         * * '<=' : Smaller than an equal
5316
         * * '>=' : Greater than an equal
5317
         * * '<' : Smaller
5318
         * * '>' : Greater
5319
         * 'in' : Array of value which are in the list of values
5320
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
5321
         *
5322
         * This does also work for multi-dimensional arrays by passing the keys
5323
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5324
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5325
         * public properties of objects or objects implementing __isset() and __get() methods.
5326
         *
5327
         * The keys of the original map are preserved in the returned map.
5328
         *
5329
         * @param string $key Key or path of the value in the array or object used for comparison
5330
         * @param string $op Operator used for comparison
5331
         * @param mixed $value Value used for comparison
5332
         * @return self<int|string,mixed> New map for fluid interface
5333
         */
5334
        public function where( string $key, string $op, $value ) : self
5335
        {
5336
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
30✔
5337

5338
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
42✔
5339
                        {
5340
                                switch( $op )
5✔
5341
                                {
5342
                                        case '-':
35✔
5343
                                                $list = (array) $value;
7✔
5344
                                                return $val >= current( $list ) && $val <= end( $list );
7✔
5345
                                        case 'in': return in_array( $val, (array) $value );
28✔
5346
                                        case '<': return $val < $value;
21✔
5347
                                        case '>': return $val > $value;
21✔
5348
                                        case '<=': return $val <= $value;
14✔
5349
                                        case '>=': return $val >= $value;
14✔
5350
                                        case '===': return $val === $value;
14✔
5351
                                        case '!==': return $val !== $value;
14✔
5352
                                        case '!=': return $val != $value;
14✔
5353
                                        default: return $val == $value;
14✔
5354
                                }
5355
                        }
5356

5357
                        return false;
7✔
5358
                } );
42✔
5359
        }
5360

5361

5362
        /**
5363
         * Returns a copy of the map with the element at the given index replaced with the given value.
5364
         *
5365
         * Examples:
5366
         *  $m = Map::from( ['a' => 1] );
5367
         *  $m->with( 2, 'b' );
5368
         *  $m->with( 'a', 2 );
5369
         *
5370
         * Results:
5371
         *  ['a' => 1, 2 => 'b']
5372
         *  ['a' => 2]
5373
         *
5374
         * The original map ($m) stays untouched!
5375
         * This method is a shortcut for calling the copy() and set() methods.
5376
         *
5377
         * @param int|string $key Array key to set or replace
5378
         * @param mixed $value New value for the given key
5379
         * @return self<int|string,mixed> New map
5380
         */
5381
        public function with( $key, $value ) : self
5382
        {
5383
                return $this->copy()->set( $key, $value );
7✔
5384
        }
5385

5386

5387
        /**
5388
         * Merges the values of all arrays at the corresponding index.
5389
         *
5390
         * Examples:
5391
         *  $en = ['one', 'two', 'three'];
5392
         *  $es = ['uno', 'dos', 'tres'];
5393
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
5394
         *
5395
         * Results:
5396
         *  [
5397
         *    [1, 'one', 'uno'],
5398
         *    [2, 'two', 'dos'],
5399
         *    [3, 'three', 'tres'],
5400
         *  ]
5401
         *
5402
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
5403
         * @return self<int|string,mixed> New map of arrays
5404
         */
5405
        public function zip( ...$arrays ) : self
5406
        {
5407
                $args = array_map( function( $items ) {
5✔
5408
                        return $this->array( $items );
7✔
5409
                }, $arrays );
7✔
5410

5411
                return new static( array_map( null, $this->list(), ...$args ) );
7✔
5412
        }
5413

5414

5415
        /**
5416
         * Returns a plain array of the given elements.
5417
         *
5418
         * @param mixed $elements List of elements or single value
5419
         * @return array<int|string,mixed> Plain array
5420
         */
5421
        protected function array( $elements ) : array
5422
        {
5423
                if( is_array( $elements ) ) {
1,652✔
5424
                        return $elements;
1,484✔
5425
                }
5426

5427
                if( $elements instanceof \Closure ) {
371✔
5428
                        return (array) $elements();
105✔
5429
                }
5430

5431
                if( $elements instanceof \Aimeos\Map ) {
266✔
5432
                        return $elements->toArray();
161✔
5433
                }
5434

5435
                if( is_iterable( $elements ) ) {
112✔
5436
                        return iterator_to_array( $elements, true );
21✔
5437
                }
5438

5439
                return $elements !== null ? [$elements] : [];
91✔
5440
        }
5441

5442

5443
        /**
5444
         * Flattens a multi-dimensional array or map into a single level array.
5445
         *
5446
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
5447
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
5448
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
5449
         */
5450
        protected function flatten( iterable $entries, array &$result, int $depth ) : void
5451
        {
5452
                foreach( $entries as $entry )
35✔
5453
                {
5454
                        if( is_iterable( $entry ) && $depth > 0 ) {
35✔
5455
                                $this->flatten( $entry, $result, $depth - 1 );
28✔
5456
                        } else {
5457
                                $result[] = $entry;
35✔
5458
                        }
5459
                }
5460
        }
10✔
5461

5462

5463
        /**
5464
         * Flattens a multi-dimensional array or map into a single level array.
5465
         *
5466
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
5467
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
5468
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
5469
         */
5470
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
5471
        {
5472
                foreach( $entries as $key => $entry )
35✔
5473
                {
5474
                        if( is_iterable( $entry ) && $depth > 0 ) {
35✔
5475
                                $this->kflatten( $entry, $result, $depth - 1 );
35✔
5476
                        } else {
5477
                                $result[$key] = $entry;
35✔
5478
                        }
5479
                }
5480
        }
10✔
5481

5482

5483
        /**
5484
         * Returns a reference to the array of elements
5485
         *
5486
         * @return array Reference to the array of elements
5487
         */
5488
        protected function &list() : array
5489
        {
5490
                if( !is_array( $this->list ) ) {
2,226✔
5491
                        $this->list = $this->array( $this->list );
×
5492
                }
5493

5494
                return $this->list;
2,226✔
5495
        }
5496

5497

5498
        /**
5499
         * Returns a configuration value from an array.
5500
         *
5501
         * @param array<mixed>|object $entry The array or object to look at
5502
         * @param array<string> $parts Path parts to look for inside the array or object
5503
         * @return mixed Found value or null if no value is available
5504
         */
5505
        protected function val( $entry, array $parts )
5506
        {
5507
                foreach( $parts as $part )
273✔
5508
                {
5509
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
273✔
5510
                                $entry = $entry[$part];
154✔
5511
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
175✔
5512
                                $entry = $entry->{$part};
7✔
5513
                        } else {
5514
                                return null;
183✔
5515
                        }
5516
                }
5517

5518
                return $entry;
154✔
5519
        }
5520

5521

5522
        /**
5523
         * Visits each entry, calls the callback and returns the items in the result argument
5524
         *
5525
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
5526
         * @param array<mixed> $result Numerically indexed list of all visited entries
5527
         * @param int $level Current depth of the nodes in the tree
5528
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
5529
         * @param string $nestKey Key to the children of each entry
5530
         */
5531
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey ) : void
5532
        {
5533
                foreach( $entries as $key => $entry )
28✔
5534
                {
5535
                        $result[] = $callback ? $callback( $entry, $key, $level ) : $entry;
28✔
5536

5537
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
28✔
5538
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey );
21✔
5539
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
7✔
5540
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey );
10✔
5541
                        }
5542
                }
5543
        }
8✔
5544
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc