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

aimeos / map / 23639766954

27 Mar 2026 09:28AM UTC coverage: 97.711%. Remained the same
23639766954

push

github

aimeos
Fixed test

811 of 830 relevant lines covered (97.71%)

18.58 hits per line

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

97.7
/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;
1,260✔
53
                $this->list = $elements;
1,260✔
54
        }
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
         * @throws \BadMethodCallException
71
         */
72
        public static function __callStatic( string $name, array $params )
73
        {
74
                if( !isset( static::$methods[$name] ) ) {
6✔
75
                        throw new \BadMethodCallException( sprintf( 'Method %s::%s does not exist.', static::class, $name ) );
3✔
76
                }
77

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

81

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

117
                $result = [];
6✔
118

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

126
                return new static( $result );
6✔
127
        }
128

129

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

140

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

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

167
                return $old;
3✔
168
        }
169

170

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

209
                $limit = $limit ?: 1;
12✔
210
                $parts = mb_str_split( $string, 1, 'UTF-8' );
12✔
211

212
                if( $limit < 1 ) {
12✔
213
                        return new static( array_slice( $parts, 0, $limit ) );
3✔
214
                }
215

216
                if( $limit < count( $parts ) )
9✔
217
                {
218
                        $result = array_slice( $parts, 0, $limit );
3✔
219
                        $result[] = join( '', array_slice( $parts, $limit ) );
3✔
220

221
                        return new static( $result );
3✔
222
                }
223

224
                return new static( $parts );
6✔
225
        }
226

227

228
        /**
229
         * Creates a new map filled with given value.
230
         *
231
         * Examples:
232
         *  Map::fill( 3, 'a' );
233
         *  Map::fill( 3, 'a', 2 );
234
         *  Map::fill( 3, 'a', -2 );
235
         *
236
         * Results:
237
         * The first example will return [0 => 'a', 1 => 'a', 2 => 'a']. The second
238
         * example will return [2 => 'a', 3 => 'a', 4 => 'a'] and the last one
239
         * [-2 => 'a', -1 => 'a', 0 => 'a'] (PHP 8) or [-2 => 'a', 0 => 'a', 1 => 'a'] (PHP 7).
240
         *
241
         * @param int $num Number of elements to create
242
         * @param mixed $value Value to fill the map with
243
         * @param int $start Start index for the elements
244
         * @return self<int|string,mixed> New map with filled elements
245
         */
246
        public static function fill( int $num, $value, int $start = 0 ) : self
247
        {
248
                return new static( array_fill( $start, $num, $value ) );
6✔
249
        }
250

251

252
        /**
253
         * Creates a new map instance if the value isn't one already.
254
         *
255
         * Examples:
256
         *  Map::from( [] );
257
         *  Map::from( null );
258
         *  Map::from( 'a' );
259
         *  Map::from( new Map() );
260
         *  Map::from( new ArrayObject() );
261
         *
262
         * Results:
263
         * A new map instance containing the list of elements. In case of an empty
264
         * array or null, the map object will contain an empty list. If a map object
265
         * is passed, it will be returned instead of creating a new instance.
266
         *
267
         * @param mixed $elements List of elements or single element
268
         * @return self<int|string,mixed> New map object
269
         */
270
        public static function from( $elements = [] ) : self
271
        {
272
                if( $elements instanceof self ) {
606✔
273
                        return $elements;
9✔
274
                }
275

276
                return new static( $elements );
606✔
277
        }
278

279

280
        /**
281
         * Creates a new map instance from a JSON string.
282
         *
283
         * This method creates a lazy Map and the string is decoded after calling
284
         * another method that operates on the Map contents. Thus, the exception in
285
         * case of an error isn't thrown immediately but after calling the next method.
286
         *
287
         * Examples:
288
         *  Map::fromJson( '["a", "b"]' );
289
         *  Map::fromJson( '{"a": "b"}' );
290
         *  Map::fromJson( '""' );
291
         *
292
         * Results:
293
         *  ['a', 'b']
294
         *  ['a' => 'b']
295
         *  ['']
296
         *
297
         * There are several options available for decoding the JSON string:
298
         * {@link https://www.php.net/manual/en/function.json-decode.php}
299
         * The parameter can be a single JSON_* constant or a bitmask of several
300
         * constants combine by bitwise OR (|), e.g.:
301
         *
302
         *  JSON_BIGINT_AS_STRING|JSON_INVALID_UTF8_IGNORE
303
         *
304
         * @param string $json JSON string to decode
305
         * @param int $options Combination of JSON_* constants
306
         * @return self<int|string,mixed> New map from decoded JSON string
307
         * @throws \RuntimeException If the passed JSON string is invalid
308
         */
309
        public static function fromJson( string $json, int $options = JSON_BIGINT_AS_STRING ) : self
310
        {
311
                if( ( $result = json_decode( $json, true, 512, $options ) ) !== null ) {
12✔
312
                        return new static( $result );
9✔
313
                }
314

315
                throw new \RuntimeException( 'Not a valid JSON string: ' . $json );
3✔
316
        }
317

318

319
        /**
320
         * Registers a custom method or returns the existing one.
321
         *
322
         * The registered method has access to the class properties if called non-static.
323
         *
324
         * Examples:
325
         *  Map::method( 'foo', function( $arg1, $arg2 ) {
326
         *      return $this->list();
327
         *  } );
328
         *
329
         * Dynamic calls have access to the class properties:
330
         *  Map::from( ['bar'] )->foo( $arg1, $arg2 );
331
         *
332
         * Static calls yield an error because $this->elements isn't available:
333
         *  Map::foo( $arg1, $arg2 );
334
         *
335
         * @param string $method Method name
336
         * @param \Closure|null $fcn Anonymous function or NULL to return the closure if available
337
         * @return \Closure|null Registered anonymous function or NULL if none has been registered
338
         */
339
        public static function method( string $method, ?\Closure $fcn = null ) : ?\Closure
340
        {
341
                if( $fcn ) {
9✔
342
                        self::$methods[$method] = $fcn;
9✔
343
                }
344

345
                return self::$methods[$method] ?? null;
9✔
346
        }
347

348

349
        /**
350
         * Creates a new map by invoking the closure the given number of times.
351
         *
352
         * This method creates a lazy Map and the entries are generated after calling
353
         * another method that operates on the Map contents. Thus, the passed callback
354
         * is not called immediately!
355
         *
356
         * Examples:
357
         *  Map::times( 3, function( $num ) {
358
         *    return $num * 10;
359
         *  } );
360
         *  Map::times( 3, function( $num, &$key ) {
361
         *    $key = $num * 2;
362
         *    return $num * 5;
363
         *  } );
364
         *  Map::times( 2, function( $num ) {
365
         *    return new \stdClass();
366
         *  } );
367
         *
368
         * Results:
369
         *  [0 => 0, 1 => 10, 2 => 20]
370
         *  [0 => 0, 2 => 5, 4 => 10]
371
         *  [0 => new \stdClass(), 1 => new \stdClass()]
372
         *
373
         * @param int $num Number of times the function is called
374
         * @param \Closure $callback Function with (value, key) parameters and returns new value
375
         * @return self<int|string,mixed> New map with the generated elements
376
         */
377
        public static function times( int $num, \Closure $callback ) : self
378
        {
379
                $list = [];
9✔
380

381
                for( $i = 0; $i < $num; $i++ ) {
9✔
382
                        $key = $i;
9✔
383
                        $list[$key] = $callback( $i, $key );
9✔
384
                }
385

386
                return new static( $list );
9✔
387
        }
388

389

390
        /**
391
         * Returns the elements after the given one.
392
         *
393
         * Examples:
394
         *  Map::from( ['a' => 1, 'b' => 0] )->after( 1 );
395
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'b' );
396
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'c' );
397
         *  Map::from( ['a', 'c', 'b'] )->after( function( $item, $key ) {
398
         *      return $item >= 'c';
399
         *  } );
400
         *
401
         * Results:
402
         *  ['b' => 0]
403
         *  [1 => 'a']
404
         *  []
405
         *  [2 => 'b']
406
         *
407
         * The keys are preserved using this method.
408
         *
409
         * @param \Closure|int|string $value Value or function with (item, key) parameters
410
         * @return self<int|string,mixed> New map with the elements after the given one
411
         */
412
        public function after( $value ) : self
413
        {
414
                if( ( $pos = $this->pos( $value ) ) === null ) {
12✔
415
                        return new static();
3✔
416
                }
417

418
                return new static( array_slice( $this->list(), $pos + 1, null, true ) );
9✔
419
        }
420

421

422
        /**
423
         * Returns the elements as a plain array.
424
         *
425
         * @return array<int|string,mixed> Plain array
426
         */
427
        public function all() : array
428
        {
429
                return $this->list = $this->array( $this->list );
×
430
        }
431

432

433
        /**
434
         * Tests if at least one element satisfies the callback function.
435
         *
436
         * Examples:
437
         *  Map::from( ['a', 'b'] )->any( function( $item, $key ) {
438
         *    return $item === 'a';
439
         *  } );
440
         *  Map::from( ['a', 'b'] )->any( function( $item, $key ) {
441
         *    return !is_string( $item );
442
         *  } );
443
         *
444
         * Results:
445
         * The first example will return TRUE while the last one will return FALSE
446
         *
447
         * @param \Closure $callback Anonymous function with (item, key) parameter
448
         * @return bool TRUE if at least one element satisfies the callback function, FALSE if not
449
         */
450
        public function any( \Closure $callback ) : bool
451
        {
452
                if( function_exists( 'array_any' ) ) {
3✔
453
                        return array_any( $this->list(), $callback );
2✔
454
                }
455

456
                foreach( $this->list() as $key => $item )
1✔
457
                {
458
                        if( $callback( $item, $key ) ) {
1✔
459
                                return true;
1✔
460
                        }
461
                }
462

463
                return false;
1✔
464
        }
465

466

467
        /**
468
         * Sorts all elements in reverse order and maintains the key association.
469
         *
470
         * Examples:
471
         *  Map::from( ['b' => 0, 'a' => 1] )->arsort();
472
         *  Map::from( ['a', 'b'] )->arsort();
473
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort();
474
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
475
         *
476
         * Results:
477
         *  ['a' => 1, 'b' => 0]
478
         *  ['b', 'a']
479
         *  [1 => 'b', 0 => 'C']
480
         *  [0 => 'C', 1 => 'b'] // because 'C' -> 'c' and 'c' > 'b'
481
         *
482
         * The parameter modifies how the values are compared. Possible parameter values are:
483
         * - SORT_REGULAR : compare elements normally (don't change types)
484
         * - SORT_NUMERIC : compare elements numerically
485
         * - SORT_STRING : compare elements as strings
486
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
487
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
488
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
489
         *
490
         * The keys are preserved using this method and no new map is created.
491
         *
492
         * @param int $options Sort options for arsort()
493
         * @return self<int|string,mixed> Updated map for fluid interface
494
         */
495
        public function arsort( int $options = SORT_REGULAR ) : self
496
        {
497
                arsort( $this->list(), $options );
12✔
498
                return $this;
12✔
499
        }
500

501

502
        /**
503
         * Sorts a copy of all elements in reverse order and maintains the key association.
504
         *
505
         * Examples:
506
         *  Map::from( ['b' => 0, 'a' => 1] )->arsorted();
507
         *  Map::from( ['a', 'b'] )->arsorted();
508
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsorted();
509
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsorted( SORT_STRING|SORT_FLAG_CASE );
510
         *
511
         * Results:
512
         *  ['a' => 1, 'b' => 0]
513
         *  ['b', 'a']
514
         *  [1 => 'b', 0 => 'C']
515
         *  [0 => 'C', 1 => 'b'] // because 'C' -> 'c' and 'c' > 'b'
516
         *
517
         * The parameter modifies how the values are compared. Possible parameter values are:
518
         * - SORT_REGULAR : compare elements normally (don't change types)
519
         * - SORT_NUMERIC : compare elements numerically
520
         * - SORT_STRING : compare elements as strings
521
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
522
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
523
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
524
         *
525
         * The keys are preserved using this method and a new map is created.
526
         *
527
         * @param int $options Sort options for arsort()
528
         * @return self<int|string,mixed> New map with sorted elements
529
         */
530
        public function arsorted( int $options = SORT_REGULAR ) : self
531
        {
532
                return ( clone $this )->arsort( $options );
3✔
533
        }
534

535

536
        /**
537
         * Sorts all elements and maintains the key association.
538
         *
539
         * Examples:
540
         *  Map::from( ['a' => 1, 'b' => 0] )->asort();
541
         *  Map::from( [0 => 'b', 1 => 'a'] )->asort();
542
         *  Map::from( [0 => 'C', 1 => 'b'] )->asort();
543
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
544
         *
545
         * Results:
546
         *  ['b' => 0, 'a' => 1]
547
         *  [1 => 'a', 0 => 'b']
548
         *  [0 => 'C', 1 => 'b'] // because 'C' < 'b'
549
         *  [1 => 'b', 0 => 'C'] // because 'C' -> 'c' and 'c' > 'b'
550
         *
551
         * The parameter modifies how the values are compared. Possible parameter values are:
552
         * - SORT_REGULAR : compare elements normally (don't change types)
553
         * - SORT_NUMERIC : compare elements numerically
554
         * - SORT_STRING : compare elements as strings
555
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
556
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
557
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
558
         *
559
         * The keys are preserved using this method and no new map is created.
560
         *
561
         * @param int $options Sort options for asort()
562
         * @return self<int|string,mixed> Updated map for fluid interface
563
         */
564
        public function asort( int $options = SORT_REGULAR ) : self
565
        {
566
                asort( $this->list(), $options );
12✔
567
                return $this;
12✔
568
        }
569

570

571
        /**
572
         * Sorts a copy of all elements and maintains the key association.
573
         *
574
         * Examples:
575
         *  Map::from( ['a' => 1, 'b' => 0] )->asorted();
576
         *  Map::from( [0 => 'b', 1 => 'a'] )->asorted();
577
         *  Map::from( [0 => 'C', 1 => 'b'] )->asorted();
578
         *  Map::from( [0 => 'C', 1 => 'b'] )->asorted( SORT_STRING|SORT_FLAG_CASE );
579
         *
580
         * Results:
581
         *  ['b' => 0, 'a' => 1]
582
         *  [1 => 'a', 0 => 'b']
583
         *  [0 => 'C', 1 => 'b'] // because 'C' < 'b'
584
         *  [1 => 'b', 0 => 'C'] // because 'C' -> 'c' and 'c' > 'b'
585
         *
586
         * The parameter modifies how the values are compared. Possible parameter values are:
587
         * - SORT_REGULAR : compare elements normally (don't change types)
588
         * - SORT_NUMERIC : compare elements numerically
589
         * - SORT_STRING : compare elements as strings
590
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
591
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
592
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
593
         *
594
         * The keys are preserved using this method and a new map is created.
595
         *
596
         * @param int $options Sort options for asort()
597
         * @return self<int|string,mixed> New map with sorted elements
598
         */
599
        public function asorted( int $options = SORT_REGULAR ) : self
600
        {
601
                return ( clone $this )->asort( $options );
3✔
602
        }
603

604

605
        /**
606
         * Returns the value at the given position.
607
         *
608
         * Examples:
609
         *  Map::from( [1, 3, 5] )->at( 0 );
610
         *  Map::from( [1, 3, 5] )->at( 1 );
611
         *  Map::from( [1, 3, 5] )->at( -1 );
612
         *  Map::from( [1, 3, 5] )->at( 3 );
613
         *
614
         * Results:
615
         * The first line will return "1", the second one "3", the third one "5" and
616
         * the last one NULL.
617
         *
618
         * The position starts from zero and a position of "0" returns the first element
619
         * of the map, "1" the second and so on. If the position is negative, the
620
         * sequence will start from the end of the map.
621
         *
622
         * @param int $pos Position of the value in the map
623
         * @return mixed|null Value at the given position or NULL if no value is available
624
         */
625
        public function at( int $pos )
626
        {
627
                $pair = array_slice( $this->list(), $pos, 1 );
3✔
628
                return !empty( $pair ) ? current( $pair ) : null;
3✔
629
        }
630

631

632
        /**
633
         * Returns the average of all integer and float values in the map.
634
         *
635
         * Examples:
636
         *  Map::from( [1, 3, 5] )->avg();
637
         *  Map::from( [1, null, 5] )->avg();
638
         *  Map::from( [1, 'sum', 5] )->avg();
639
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->avg( 'p' );
640
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->avg( 'i/p' );
641
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->avg( fn( $val, $key ) => $val['i']['p'] ?? null );
642
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->avg( fn( $val, $key ) => $key < 1 ? $val : null );
643
         *
644
         * Results:
645
         * The first and second line will return "3", the third one "2", the forth
646
         * one "30", the fifth and sixth one "40" and the last one "30".
647
         *
648
         * Non-numeric values will be removed before calculation.
649
         *
650
         * NULL values are treated as 0, non-numeric values will generate an error.
651
         *
652
         * This does also work for multi-dimensional arrays by passing the keys
653
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
654
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
655
         * public properties of objects or objects implementing __isset() and __get() methods.
656
         *
657
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to compute the average for
658
         * @return float Average of all elements or 0 if there are no elements in the map
659
         */
660
        public function avg( $col = null ) : float
661
        {
662
                $list = $this->list();
9✔
663
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, 'is_numeric' );
9✔
664

665
                return !empty( $vals ) ? array_sum( $vals ) / count( $vals ) : 0;
9✔
666
        }
667

668

669
        /**
670
         * Returns the elements before the given one.
671
         *
672
         * Examples:
673
         *  Map::from( ['a' => 1, 'b' => 0] )->before( 0 );
674
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'a' );
675
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'b' );
676
         *  Map::from( ['a', 'c', 'b'] )->before( function( $item, $key ) {
677
         *      return $key >= 1;
678
         *  } );
679
         *
680
         * Results:
681
         *  ['a' => 1]
682
         *  [0 => 'b']
683
         *  []
684
         *  [0 => 'a']
685
         *
686
         * The keys are preserved using this method.
687
         *
688
         * @param \Closure|int|string $value Value or function with (item, key) parameters
689
         * @return self<int|string,mixed> New map with the elements before the given one
690
         */
691
        public function before( $value ) : self
692
        {
693
                if( ( $pos = $this->pos( $value ) ) === null ) {
12✔
694
                        return new static();
×
695
                }
696

697
                return new static( array_slice( $this->list(), 0, $pos, true ) );
12✔
698
        }
699

700

701
        /**
702
         * Returns an element by key and casts it to boolean if possible.
703
         *
704
         * Examples:
705
         *  Map::from( ['a' => true] )->bool( 'a' );
706
         *  Map::from( ['a' => '1'] )->bool( 'a' );
707
         *  Map::from( ['a' => 1.1] )->bool( 'a' );
708
         *  Map::from( ['a' => '10'] )->bool( 'a' );
709
         *  Map::from( ['a' => 'abc'] )->bool( 'a' );
710
         *  Map::from( ['a' => ['b' => ['c' => true]]] )->bool( 'a/b/c' );
711
         *  Map::from( [] )->bool( 'c', function() { return rand( 1, 2 ); } );
712
         *  Map::from( [] )->bool( 'a', true );
713
         *
714
         *  Map::from( [] )->bool( 'b' );
715
         *  Map::from( ['b' => ''] )->bool( 'b' );
716
         *  Map::from( ['b' => null] )->bool( 'b' );
717
         *  Map::from( ['b' => [true]] )->bool( 'b' );
718
         *  Map::from( ['b' => resource] )->bool( 'b' );
719
         *  Map::from( ['b' => new \stdClass] )->bool( 'b' );
720
         *
721
         *  Map::from( [] )->bool( 'c', new \Exception( 'error' ) );
722
         *
723
         * Results:
724
         * The first eight examples will return TRUE while the 9th to 14th example
725
         * returns FALSE. The last example will throw an exception.
726
         *
727
         * This does also work for multi-dimensional arrays by passing the keys
728
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
729
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
730
         * public properties of objects or objects implementing __isset() and __get() methods.
731
         *
732
         * @param int|string $key Key or path to the requested item
733
         * @param mixed $default Default value if key isn't found (will be casted to bool)
734
         * @return bool Value from map or default value
735
         */
736
        public function bool( $key, $default = false ) : bool
737
        {
738
                return (bool) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
9✔
739
        }
740

741

742
        /**
743
         * Calls the given method on all items and returns the result.
744
         *
745
         * This method can call methods on the map entries that are also implemented
746
         * by the map object itself and are therefore not reachable when using the
747
         * magic __call() method.
748
         *
749
         * Examples:
750
         *  $item = new MyClass(); // implements methods get() and toArray()
751
         *  Map::from( [$item, $item] )->call( 'get', ['myprop'] );
752
         *  Map::from( [$item, $item] )->call( 'toArray' );
753
         *
754
         * Results:
755
         * The first example will return ['...', '...'] while the second one returns [[...], [...]].
756
         *
757
         * If some entries are not objects, they will be skipped. The map keys from the
758
         * original map are preserved in the returned map.
759
         *
760
         * @param string $name Method name
761
         * @param array<mixed> $params List of parameters
762
         * @return self<int|string,mixed> New map with results from all elements
763
         */
764
        public function call( string $name, array $params = [] ) : self
765
        {
766
                $result = [];
3✔
767

768
                foreach( $this->list() as $key => $item )
3✔
769
                {
770
                        if( is_object( $item ) ) {
3✔
771
                                $result[$key] = $item->{$name}( ...$params );
3✔
772
                        }
773
                }
774

775
                return new static( $result );
3✔
776
        }
777

778

779
        /**
780
         * Casts all entries to the passed type.
781
         *
782
         * Examples:
783
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast();
784
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'bool' );
785
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'int' );
786
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'float' );
787
         *  Map::from( [new stdClass, new stdClass] )->cast( 'array' );
788
         *  Map::from( [[], []] )->cast( 'object' );
789
         *
790
         * Results:
791
         * The examples will return (in this order):
792
         * ['1', '1', '1.0', 'yes']
793
         * [true, true, true, true]
794
         * [1, 1, 1, 0]
795
         * [1.0, 1.0, 1.0, 0.0]
796
         * [[], []]
797
         * [new stdClass, new stdClass]
798
         *
799
         * Casting arrays and objects to scalar values won't return anything useful!
800
         *
801
         * @param string $type Type to cast the values to ("string", "bool", "int", "float", "array", "object")
802
         * @return self<int|string,mixed> Updated map with casted elements
803
         */
804
        public function cast( string $type = 'string' ) : self
805
        {
806
                foreach( $this->list() as &$item )
3✔
807
                {
808
                        switch( $type )
809
                        {
810
                                case 'bool': $item = (bool) $item; break;
3✔
811
                                case 'int': $item = (int) $item; break;
3✔
812
                                case 'float': $item = (float) $item; break;
3✔
813
                                case 'string': $item = (string) $item; break;
3✔
814
                                case 'array': $item = (array) $item; break;
3✔
815
                                case 'object': $item = (object) $item; break;
3✔
816
                        }
817
                }
818

819
                return $this;
3✔
820
        }
821

822

823
        /**
824
         * Chunks the map into arrays with the given number of elements.
825
         *
826
         * Examples:
827
         *  Map::from( [0, 1, 2, 3, 4] )->chunk( 3 );
828
         *  Map::from( ['a' => 0, 'b' => 1, 'c' => 2] )->chunk( 2 );
829
         *
830
         * Results:
831
         *  [[0, 1, 2], [3, 4]]
832
         *  [['a' => 0, 'b' => 1], ['c' => 2]]
833
         *
834
         * The last chunk may contain less elements than the given number.
835
         *
836
         * The sub-arrays of the returned map are plain PHP arrays. If you need Map
837
         * objects, then wrap them with Map::from() when you iterate over the map.
838
         *
839
         * @param int $size Maximum size of the sub-arrays
840
         * @param bool $preserve Preserve keys in new map
841
         * @return self<int|string,mixed> New map with elements chunked in sub-arrays
842
         * @throws \InvalidArgumentException If size is smaller than 1
843
         */
844
        public function chunk( int $size, bool $preserve = false ) : self
845
        {
846
                if( $size < 1 ) {
9✔
847
                        throw new \InvalidArgumentException( 'Chunk size must be greater or equal than 1' );
3✔
848
                }
849

850
                return new static( array_chunk( $this->list(), $size, $preserve ) );
6✔
851
        }
852

853

854
        /**
855
         * Removes all elements from the current map.
856
         *
857
         * @return self<int|string,mixed> Updated map for fluid interface
858
         */
859
        public function clear() : self
860
        {
861
                $this->list = [];
12✔
862
                return $this;
12✔
863
        }
864

865

866
        /**
867
         * Clones the map and all objects within.
868
         *
869
         * Examples:
870
         *  Map::from( [new \stdClass, new \stdClass] )->clone();
871
         *
872
         * Results:
873
         *   [new \stdClass, new \stdClass]
874
         *
875
         * The objects within the Map are NOT the same as before but new cloned objects.
876
         * This is different to copy(), which doesn't clone the objects within.
877
         *
878
         * The keys are preserved using this method.
879
         *
880
         * @return self<int|string,mixed> New map with cloned objects
881
         */
882
        public function clone() : self
883
        {
884
                $list = [];
3✔
885

886
                foreach( $this->list() as $key => $item ) {
3✔
887
                        $list[$key] = is_object( $item ) ? clone $item : $item;
3✔
888
                }
889

890
                return new static( $list );
3✔
891
        }
892

893

894
        /**
895
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
896
         *
897
         * Examples:
898
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val' );
899
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val', 'id' );
900
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( null, 'id' );
901
         *  Map::from( [['id' => 'ix', 'val' => 'v1'], ['id' => 'ix', 'val' => 'v2']] )->col( null, 'id' );
902
         *  Map::from( [['foo' => ['bar' => 'one', 'baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
903
         *  Map::from( [['foo' => ['bar' => 'one']]] )->col( 'foo/baz', 'foo/bar' );
904
         *  Map::from( [['foo' => ['baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
905
         *
906
         * Results:
907
         *  ['v1', 'v2']
908
         *  ['i1' => 'v1', 'i2' => 'v2']
909
         *  ['i1' => ['id' => 'i1', 'val' => 'v1'], 'i2' => ['id' => 'i2', 'val' => 'v2']]
910
         *  ['ix' => ['id' => 'ix', 'val' => 'v2']]
911
         *  ['one' => 'two']
912
         *  ['one' => null]
913
         *  ['two']
914
         *
915
         * If $indexcol is omitted, it's value is NULL or not set, the result will be indexed from 0-n.
916
         * Items with the same value for $indexcol will overwrite previous items and only the last
917
         * one will be part of the resulting map.
918
         *
919
         * This does also work to map values from multi-dimensional arrays by passing the keys
920
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
921
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
922
         * public properties of objects or objects implementing __isset() and __get() methods.
923
         *
924
         * @param string|null $valuecol Name or path of the value property
925
         * @param string|null $indexcol Name or path of the index property
926
         * @return self<int|string,mixed> New map with mapped entries
927
         */
928
        public function col( ?string $valuecol = null, ?string $indexcol = null ) : self
929
        {
930
                $vparts = explode( $this->sep, (string) $valuecol );
27✔
931
                $iparts = explode( $this->sep, (string) $indexcol );
27✔
932

933
                if( ( $valuecol === null || count( $vparts ) === 1 )
27✔
934
                        && ( $indexcol === null || count( $iparts ) === 1 )
27✔
935
                ) {
936
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
18✔
937
                }
938

939
                $list = [];
9✔
940

941
                foreach( $this->list() as $key => $item )
9✔
942
                {
943
                        $v = $valuecol ? $this->val( $item, $vparts ) : $item;
9✔
944

945
                        if( $indexcol && ( $k = (string) $this->val( $item, $iparts ) ) ) {
9✔
946
                                $list[$k] = $v;
6✔
947
                        } else {
948
                                $list[$key] = $v;
3✔
949
                        }
950
                }
951

952
                return new static( $list );
9✔
953
        }
954

955

956
        /**
957
         * Collapses all sub-array elements recursively to a new map overwriting existing keys.
958
         *
959
         * Examples:
960
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['c' => 2, 'd' => 3]] )->collapse();
961
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['a' => 2]] )->collapse();
962
         *  Map::from( [0 => [0 => 0, 1 => 1], 1 => [0 => ['a' => 2, 0 => 3], 1 => 4]] )->collapse();
963
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => [0 => ['b' => 2, 0 => 3], 1 => 4]] )->collapse( 1 );
964
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => Map::from( [0 => ['b' => 2, 0 => 3], 1 => 4] )] )->collapse();
965
         *
966
         * Results:
967
         *  ['a' => 0, 'b' => 1, 'c' => 2, 'd' => 3]
968
         *  ['a' => 2, 'b' => 1]
969
         *  [0 => 3, 1 => 4, 'a' => 2]
970
         *  [0 => ['b' => 2, 0 => 3], 1 => 4, 'a' => 1]
971
         *  [0 => 3, 'a' => 1, 'b' => 2, 1 => 4]
972
         *
973
         * The keys are preserved and already existing elements will be overwritten.
974
         * This is also true for numeric keys! A value smaller than 1 for depth will
975
         * return the same map elements. Collapsing does also work if elements
976
         * implement the "Traversable" interface (which the Map object does).
977
         *
978
         * This method is similar than flat() but replaces already existing elements.
979
         *
980
         * @param int|null $depth Number of levels to collapse for multi-dimensional arrays or NULL for all
981
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
982
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
983
         */
984
        public function collapse( ?int $depth = null ) : self
985
        {
986
                if( $depth < 0 ) {
18✔
987
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
3✔
988
                }
989

990
                $result = [];
15✔
991
                $this->kflatten( $this->list(), $result, $depth ?? 0x7fffffff );
15✔
992
                return new static( $result );
15✔
993
        }
994

995

996
        /**
997
         * Combines the values of the map as keys with the passed elements as values.
998
         *
999
         * Examples:
1000
         *  Map::from( ['name', 'age'] )->combine( ['Tom', 29] );
1001
         *
1002
         * Results:
1003
         *  ['name' => 'Tom', 'age' => 29]
1004
         *
1005
         * @param iterable<int|string,mixed> $values Values of the new map
1006
         * @return self<int|string,mixed> New map
1007
         */
1008
        public function combine( iterable $values ) : self
1009
        {
1010
                return new static( array_combine( $this->list(), $this->array( $values ) ) );
3✔
1011
        }
1012

1013

1014
        /**
1015
         * Compares the value against all map elements.
1016
         *
1017
         * This method is an alias for strCompare().
1018
         *
1019
         * @param string $value Value to compare map elements to
1020
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
1021
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
1022
         * @deprecated Use strCompare() method instead
1023
         */
1024
        public function compare( string $value, bool $case = true ) : bool
1025
        {
1026
                return $this->strCompare( $value, $case );
3✔
1027
        }
1028

1029

1030
        /**
1031
         * Pushs all of the given elements onto the map with new keys without creating a new map.
1032
         *
1033
         * Examples:
1034
         *  Map::from( ['foo'] )->concat( new Map( ['bar'] ));
1035
         *
1036
         * Results:
1037
         *  ['foo', 'bar']
1038
         *
1039
         * The keys of the passed elements are NOT preserved!
1040
         *
1041
         * @param iterable<int|string,mixed> $elements List of elements
1042
         * @return self<int|string,mixed> Updated map for fluid interface
1043
         */
1044
        public function concat( iterable $elements ) : self
1045
        {
1046
                $this->list();
6✔
1047

1048
                foreach( $elements as $item ) {
6✔
1049
                        $this->list[] = $item;
6✔
1050
                }
1051

1052
                return $this;
6✔
1053
        }
1054

1055

1056
        /**
1057
         * Determines if an item exists in the map.
1058
         *
1059
         * This method combines the power of the where() method with some() to check
1060
         * if the map contains at least one of the passed values or conditions.
1061
         *
1062
         * Examples:
1063
         *  Map::from( ['a', 'b'] )->contains( 'a' );
1064
         *  Map::from( ['a', 'b'] )->contains( ['a', 'c'] );
1065
         *  Map::from( ['a', 'b'] )->contains( function( $item, $key ) {
1066
         *    return $item === 'a'
1067
         *  } );
1068
         *  Map::from( [['type' => 'name']] )->contains( 'type', 'name' );
1069
         *  Map::from( [['type' => 'name']] )->contains( 'type', '==', 'name' );
1070
         *
1071
         * Results:
1072
         * All method calls will return TRUE because at least "a" is included in the
1073
         * map or there's a "type" key with a value "name" like in the last two
1074
         * examples.
1075
         *
1076
         * Check the where() method for available operators.
1077
         *
1078
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
1079
         * @param string|null $op Operator used for comparison
1080
         * @param mixed $value Value used for comparison
1081
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
1082
         */
1083
        public function contains( $key, ?string $operator = null, $value = null ) : bool
1084
        {
1085
                if( $operator === null ) {
6✔
1086
                        return $this->some( $key );
3✔
1087
                }
1088

1089
                if( $value === null ) {
3✔
1090
                        return !$this->where( $key, '==', $operator )->isEmpty();
3✔
1091
                }
1092

1093
                return !$this->where( $key, $operator, $value )->isEmpty();
3✔
1094
        }
1095

1096

1097
        /**
1098
         * Creates a new map with the same elements.
1099
         *
1100
         * Both maps share the same array until one of the map objects modifies the
1101
         * array. Then, the array is copied and the copy is modfied (copy on write).
1102
         *
1103
         * @return self<int|string,mixed> New map
1104
         */
1105
        public function copy() : self
1106
        {
1107
                return clone $this;
9✔
1108
        }
1109

1110

1111
        /**
1112
         * Counts the total number of elements in the map.
1113
         *
1114
         * @return int Number of elements
1115
         */
1116
        public function count() : int
1117
        {
1118
                return count( $this->list() );
51✔
1119
        }
1120

1121

1122
        /**
1123
         * Counts how often the same values are in the map.
1124
         *
1125
         * Examples:
1126
         *  Map::from( [1, 'foo', 2, 'foo', 1] )->countBy();
1127
         *  Map::from( [1.11, 3.33, 3.33, 9.99] )->countBy();
1128
         *  Map::from( [['i' => ['p' => 1.11]], ['i' => ['p' => 3.33]], ['i' => ['p' => 3.33]]] )->countBy( 'i/p' );
1129
         *  Map::from( ['a@gmail.com', 'b@yahoo.com', 'c@gmail.com'] )->countBy( function( $email ) {
1130
         *    return substr( strrchr( $email, '@' ), 1 );
1131
         *  } );
1132
         *
1133
         * Results:
1134
         *  [1 => 2, 'foo' => 2, 2 => 1]
1135
         *  ['1.11' => 1, '3.33' => 2, '9.99' => 1]
1136
         *  ['1.11' => 1, '3.33' => 2]
1137
         *  ['gmail.com' => 2, 'yahoo.com' => 1]
1138
         *
1139
         * Counting values does only work for integers and strings because these are
1140
         * the only types allowed as array keys. All elements are casted to strings
1141
         * if no callback is passed. Custom callbacks need to make sure that only
1142
         * string or integer values are returned!
1143
         *
1144
         * This does also work for multi-dimensional arrays by passing the keys
1145
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1146
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1147
         * public properties of objects or objects implementing __isset() and __get() methods.
1148
         *
1149
         * @param \Closure|string|null $col Key as "key1/key2/key3" or function with (value) parameter returning the values for counting
1150
         * @return self<int|string,mixed> New map with values as keys and their count as value
1151
         */
1152
        public function countBy( $col = null ) : self
1153
        {
1154
                if( !( $col instanceof \Closure ) )
12✔
1155
                {
1156
                        $parts = $col ? explode( $this->sep, (string) $col ) : [];
9✔
1157

1158
                        $col = function( $item ) use ( $parts ) {
9✔
1159
                                return (string) $this->val( $item, $parts );
9✔
1160
                        };
9✔
1161
                }
1162

1163
                return new static( array_count_values( array_map( $col, $this->list() ) ) );
12✔
1164
        }
1165

1166

1167
        /**
1168
         * Dumps the map content and terminates the script.
1169
         *
1170
         * The dd() method is very helpful to see what are the map elements passed
1171
         * between two map methods in a method call chain. It stops execution of the
1172
         * script afterwards to avoid further output.
1173
         *
1174
         * Examples:
1175
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->sort()->dd();
1176
         *
1177
         * Results:
1178
         *  Array
1179
         *  (
1180
         *      [0] => bar
1181
         *      [1] => foo
1182
         *  )
1183
         *
1184
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1185
         */
1186
        public function dd( ?callable $callback = null ) : void
1187
        {
1188
                $this->dump( $callback );
×
1189
                exit( 1 );
×
1190
        }
1191

1192

1193
        /**
1194
         * Returns the keys/values in the map whose values are not present in the passed elements in a new map.
1195
         *
1196
         * Examples:
1197
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diff( ['bar'] );
1198
         *
1199
         * Results:
1200
         *  ['a' => 'foo']
1201
         *
1202
         * If a callback is passed, the given function will be used to compare the values.
1203
         * The function must accept two parameters (value A and B) and must return
1204
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
1205
         * greater than value B. Both, a method name and an anonymous function can be passed:
1206
         *
1207
         *  Map::from( [0 => 'a'] )->diff( [0 => 'A'], 'strcasecmp' );
1208
         *  Map::from( ['b' => 'a'] )->diff( ['B' => 'A'], 'strcasecmp' );
1209
         *  Map::from( ['b' => 'a'] )->diff( ['c' => 'A'], function( $valA, $valB ) {
1210
         *      return strtolower( $valA ) <=> strtolower( $valB );
1211
         *  } );
1212
         *
1213
         * All examples will return an empty map because both contain the same values
1214
         * when compared case insensitive.
1215
         *
1216
         * The keys are preserved using this method.
1217
         *
1218
         * @param iterable<int|string,mixed> $elements List of elements
1219
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
1220
         * @return self<int|string,mixed> New map
1221
         */
1222
        public function diff( iterable $elements, ?callable $callback = null ) : self
1223
        {
1224
                if( $callback ) {
9✔
1225
                        return new static( array_udiff( $this->list(), $this->array( $elements ), $callback ) );
3✔
1226
                }
1227

1228
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
9✔
1229
        }
1230

1231

1232
        /**
1233
         * Returns the keys/values in the map whose keys AND values are not present in the passed elements in a new map.
1234
         *
1235
         * Examples:
1236
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diffAssoc( new Map( ['foo', 'b' => 'bar'] ) );
1237
         *
1238
         * Results:
1239
         *  ['a' => 'foo']
1240
         *
1241
         * If a callback is passed, the given function will be used to compare the values.
1242
         * The function must accept two parameters (value A and B) and must return
1243
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
1244
         * greater than value B. Both, a method name and an anonymous function can be passed:
1245
         *
1246
         *  Map::from( [0 => 'a'] )->diffAssoc( [0 => 'A'], 'strcasecmp' );
1247
         *  Map::from( ['b' => 'a'] )->diffAssoc( ['B' => 'A'], 'strcasecmp' );
1248
         *  Map::from( ['b' => 'a'] )->diffAssoc( ['c' => 'A'], function( $valA, $valB ) {
1249
         *      return strtolower( $valA ) <=> strtolower( $valB );
1250
         *  } );
1251
         *
1252
         * The first example will return an empty map because both contain the same
1253
         * values when compared case insensitive. The second and third example will return
1254
         * an empty map because 'A' is part of the passed array but the keys doesn't match
1255
         * ("b" vs. "B" and "b" vs. "c").
1256
         *
1257
         * The keys are preserved using this method.
1258
         *
1259
         * @param iterable<int|string,mixed> $elements List of elements
1260
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
1261
         * @return self<int|string,mixed> New map
1262
         */
1263
        public function diffAssoc( iterable $elements, ?callable $callback = null ) : self
1264
        {
1265
                if( $callback ) {
6✔
1266
                        return new static( array_diff_uassoc( $this->list(), $this->array( $elements ), $callback ) );
3✔
1267
                }
1268

1269
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
6✔
1270
        }
1271

1272

1273
        /**
1274
         * Returns the key/value pairs from the map whose keys are not present in the passed elements in a new map.
1275
         *
1276
         * Examples:
1277
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diffKeys( new Map( ['foo', 'b' => 'baz'] ) );
1278
         *
1279
         * Results:
1280
         *  ['a' => 'foo']
1281
         *
1282
         * If a callback is passed, the given function will be used to compare the keys.
1283
         * The function must accept two parameters (key A and B) and must return
1284
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
1285
         * greater than key B. Both, a method name and an anonymous function can be passed:
1286
         *
1287
         *  Map::from( [0 => 'a'] )->diffKeys( [0 => 'A'], 'strcasecmp' );
1288
         *  Map::from( ['b' => 'a'] )->diffKeys( ['B' => 'X'], 'strcasecmp' );
1289
         *  Map::from( ['b' => 'a'] )->diffKeys( ['c' => 'a'], function( $keyA, $keyB ) {
1290
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
1291
         *  } );
1292
         *
1293
         * The first and second example will return an empty map because both contain
1294
         * the same keys when compared case insensitive. The third example will return
1295
         * ['b' => 'a'] because the keys doesn't match ("b" vs. "c").
1296
         *
1297
         * The keys are preserved using this method.
1298
         *
1299
         * @param iterable<int|string,mixed> $elements List of elements
1300
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
1301
         * @return self<int|string,mixed> New map
1302
         */
1303
        public function diffKeys( iterable $elements, ?callable $callback = null ) : self
1304
        {
1305
                if( $callback ) {
6✔
1306
                        return new static( array_diff_ukey( $this->list(), $this->array( $elements ), $callback ) );
3✔
1307
                }
1308

1309
                return new static( array_diff_key( $this->list(), $this->array( $elements ) ) );
6✔
1310
        }
1311

1312

1313
        /**
1314
         * Dumps the map content using the given function (print_r by default).
1315
         *
1316
         * The dump() method is very helpful to see what are the map elements passed
1317
         * between two map methods in a method call chain.
1318
         *
1319
         * Examples:
1320
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->dump()->asort()->dump( 'var_dump' );
1321
         *
1322
         * Results:
1323
         *  Array
1324
         *  (
1325
         *      [a] => foo
1326
         *      [b] => bar
1327
         *  )
1328
         *  array(1) {
1329
         *    ["b"]=>
1330
         *    string(3) "bar"
1331
         *    ["a"]=>
1332
         *    string(3) "foo"
1333
         *  }
1334
         *
1335
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1336
         * @return self<int|string,mixed> Same map for fluid interface
1337
         */
1338
        public function dump( ?callable $callback = null ) : self
1339
        {
1340
                $callback ? $callback( $this->list() ) : print_r( $this->list() );
3✔
1341
                return $this;
3✔
1342
        }
1343

1344

1345
        /**
1346
         * Returns the duplicate values from the map.
1347
         *
1348
         * For nested arrays, you have to pass the name of the column of the nested
1349
         * array which should be used to check for duplicates.
1350
         *
1351
         * Examples:
1352
         *  Map::from( [1, 2, '1', 3] )->duplicates()
1353
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->duplicates( 'p' )
1354
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( 'i/p' )
1355
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( fn( $item, $key ) => $item['i']['p'] )
1356
         *
1357
         * Results:
1358
         *  [2 => '1']
1359
         *  [1 => ['p' => 1]]
1360
         *  [1 => ['i' => ['p' => 1]]]
1361
         *  [1 => ['i' => ['p' => 1]]]
1362
         *
1363
         * This does also work for multi-dimensional arrays by passing the keys
1364
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1365
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1366
         * public properties of objects or objects implementing __isset() and __get() methods.
1367
         *
1368
         * The keys are preserved using this method.
1369
         *
1370
         * @param \Closure|string|null $col Key, path of the nested array or anonymous function with ($item, $key) parameters returning the value for comparison
1371
         * @return self<int|string,mixed> New map
1372
         */
1373
        public function duplicates( $col = null ) : self
1374
        {
1375
                $list = $map = $this->list();
12✔
1376

1377
                if( $col !== null ) {
12✔
1378
                        $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
9✔
1379
                }
1380

1381
                return new static( array_diff_key( $list, array_unique( $map ) ) );
12✔
1382
        }
1383

1384

1385
        /**
1386
         * Executes a callback over each entry until FALSE is returned.
1387
         *
1388
         * Examples:
1389
         *  $result = [];
1390
         *  Map::from( [0 => 'a', 1 => 'b'] )->each( function( $value, $key ) use ( &$result ) {
1391
         *      $result[$key] = strtoupper( $value );
1392
         *      return false;
1393
         *  } );
1394
         *
1395
         * The $result array will contain [0 => 'A'] because FALSE is returned
1396
         * after the first entry and all other entries are then skipped.
1397
         *
1398
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1399
         * @return self<int|string,mixed> Same map for fluid interface
1400
         */
1401
        public function each( \Closure $callback ) : self
1402
        {
1403
                foreach( $this->list() as $key => $item )
6✔
1404
                {
1405
                        if( $callback( $item, $key ) === false ) {
6✔
1406
                                break;
3✔
1407
                        }
1408
                }
1409

1410
                return $this;
6✔
1411
        }
1412

1413

1414
        /**
1415
         * Determines if the map is empty or not.
1416
         *
1417
         * Examples:
1418
         *  Map::from( [] )->empty();
1419
         *  Map::from( ['a'] )->empty();
1420
         *
1421
         * Results:
1422
         *  The first example returns TRUE while the second returns FALSE
1423
         *
1424
         * The method is equivalent to isEmpty().
1425
         *
1426
         * @return bool TRUE if map is empty, FALSE if not
1427
         */
1428
        public function empty() : bool
1429
        {
1430
                return empty( $this->list() );
6✔
1431
        }
1432

1433

1434
        /**
1435
         * Tests if the passed elements are equal to the elements in the map.
1436
         *
1437
         * Examples:
1438
         *  Map::from( ['a'] )->equals( ['a', 'b'] );
1439
         *  Map::from( ['a', 'b'] )->equals( ['b'] );
1440
         *  Map::from( ['a', 'b'] )->equals( ['b', 'a'] );
1441
         *
1442
         * Results:
1443
         * The first and second example will return FALSE, the third example will return TRUE
1444
         *
1445
         * The method differs to is() in the fact that it doesn't care about the keys
1446
         * by default. The elements are only loosely compared and the keys are ignored.
1447
         *
1448
         * Values are compared by their string values:
1449
         * (string) $item1 === (string) $item2
1450
         *
1451
         * @param iterable<int|string,mixed> $elements List of elements to test against
1452
         * @return bool TRUE if both are equal, FALSE if not
1453
         */
1454
        public function equals( iterable $elements ) : bool
1455
        {
1456
                $list = $this->list();
18✔
1457
                $elements = $this->array( $elements );
18✔
1458

1459
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
18✔
1460
        }
1461

1462

1463
        /**
1464
         * Verifies that all elements pass the test of the given callback.
1465
         *
1466
         * Examples:
1467
         *  Map::from( [0 => 'a', 1 => 'b'] )->every( function( $value, $key ) {
1468
         *      return is_string( $value );
1469
         *  } );
1470
         *
1471
         *  Map::from( [0 => 'a', 1 => 100] )->every( function( $value, $key ) {
1472
         *      return is_string( $value );
1473
         *  } );
1474
         *
1475
         * The first example will return TRUE because all values are a string while
1476
         * the second example will return FALSE.
1477
         *
1478
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1479
         * @return bool True if all elements pass the test, false if if fails for at least one element
1480
         */
1481
        public function every( \Closure $callback ) : bool
1482
        {
1483
                foreach( $this->list() as $key => $item )
3✔
1484
                {
1485
                        if( $callback( $item, $key ) === false ) {
3✔
1486
                                return false;
3✔
1487
                        }
1488
                }
1489

1490
                return true;
3✔
1491
        }
1492

1493

1494
        /**
1495
         * Returns a new map without the passed element keys.
1496
         *
1497
         * Examples:
1498
         *  Map::from( ['a' => 1, 'b' => 2, 'c' => 3] )->except( 'b' );
1499
         *  Map::from( [1 => 'a', 2 => 'b', 3 => 'c'] )->except( [1, 3] );
1500
         *
1501
         * Results:
1502
         *  ['a' => 1, 'c' => 3]
1503
         *  [2 => 'b']
1504
         *
1505
         * The keys in the result map are preserved.
1506
         *
1507
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
1508
         * @return self<int|string,mixed> New map
1509
         */
1510
        public function except( $keys ) : self
1511
        {
1512
                return ( clone $this )->remove( $keys );
3✔
1513
        }
1514

1515

1516
        /**
1517
         * Applies a filter to all elements of the map and returns a new map.
1518
         *
1519
         * Examples:
1520
         *  Map::from( [null, 0, 1, '', '0', 'a'] )->filter();
1521
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->filter( function( $value, $key ) {
1522
         *      return $key < 10 && $value < 'n';
1523
         *  } );
1524
         *
1525
         * Results:
1526
         *  [1, 'a']
1527
         *  ['a', 'b']
1528
         *
1529
         * If no callback is passed, all values which are empty, null or false will be
1530
         * removed if their value converted to boolean is FALSE:
1531
         *  (bool) $value === false
1532
         *
1533
         * The keys in the result map are preserved.
1534
         *
1535
         * @param  callable|null $callback Function with (item, key) parameters and returns TRUE/FALSE
1536
         * @return self<int|string,mixed> New map
1537
         */
1538
        public function filter( ?callable $callback = null ) : self
1539
        {
1540
                if( $callback ) {
60✔
1541
                        return new static( array_filter( $this->list(), $callback, ARRAY_FILTER_USE_BOTH ) );
57✔
1542
                }
1543

1544
                return new static( array_filter( $this->list() ) );
3✔
1545
        }
1546

1547

1548
        /**
1549
         * Returns the first/last matching element where the callback returns TRUE.
1550
         *
1551
         * Examples:
1552
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1553
         *      return $value >= 'b';
1554
         *  } );
1555
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1556
         *      return $value >= 'b';
1557
         *  }, null, true );
1558
         *  Map::from( [] )->find( function( $value, $key ) {
1559
         *      return $value >= 'b';
1560
         *  }, fn() => 'none' );
1561
         *  Map::from( [] )->find( function( $value, $key ) {
1562
         *      return $value >= 'b';
1563
         *  }, new \Exception( 'error' ) );
1564
         *
1565
         * Results:
1566
         * The first example will return 'c' while the second will return 'e' (last element).
1567
         * The third and forth one will return "none" and the last one will throw the exception.
1568
         *
1569
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1570
         * @param mixed $default Default value, closure or exception if the callback only returns FALSE
1571
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1572
         * @return mixed First matching value, passed default value or an exception
1573
         */
1574
        public function find( \Closure $callback, $default = null, bool $reverse = false )
1575
        {
1576
                $list = $this->list();
15✔
1577

1578
                if( !empty( $list ) )
15✔
1579
                {
1580
                        if( $reverse )
15✔
1581
                        {
1582
                                $value = end( $list );
6✔
1583
                                $key = key( $list );
6✔
1584

1585
                                do
1586
                                {
1587
                                        if( $callback( $value, $key ) ) {
6✔
1588
                                                return $value;
3✔
1589
                                        }
1590
                                }
1591
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
6✔
1592

1593
                                reset( $list );
3✔
1594
                        }
1595
                        else
1596
                        {
1597
                                foreach( $list as $key => $value )
9✔
1598
                                {
1599
                                        if( $callback( $value, $key ) ) {
9✔
1600
                                                return $value;
3✔
1601
                                        }
1602
                                }
1603
                        }
1604
                }
1605

1606
                if( $default instanceof \Closure ) {
9✔
1607
                        return $default();
3✔
1608
                }
1609

1610
                if( $default instanceof \Throwable ) {
6✔
1611
                        throw $default;
3✔
1612
                }
1613

1614
                return $default;
3✔
1615
        }
1616

1617

1618
        /**
1619
         * Returns the first/last key where the callback returns TRUE.
1620
         *
1621
         * Examples:
1622
         *  Map::from( ['a', 'c', 'e'] )->findKey( function( $value, $key ) {
1623
         *      return $value >= 'b';
1624
         *  } );
1625
         *  Map::from( ['a', 'c', 'e'] )->findKey( function( $value, $key ) {
1626
         *      return $value >= 'b';
1627
         *  }, null, true );
1628
         *  Map::from( [] )->findKey( function( $value, $key ) {
1629
         *      return $value >= 'b';
1630
         *  }, 'none' );
1631
         *  Map::from( [] )->findKey( function( $value, $key ) {
1632
         *      return $value >= 'b';
1633
         *  }, fn() => 'none' );
1634
         *  Map::from( [] )->findKey( function( $value, $key ) {
1635
         *      return $value >= 'b';
1636
         *  }, new \Exception( 'error' ) );
1637
         *
1638
         * Results:
1639
         * The first example will return '1' while the second will return '2' (last element).
1640
         * The third and forth one will return "none" and the last one will throw the exception.
1641
         *
1642
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1643
         * @param mixed $default Default value, closure or exception if the callback only returns FALSE
1644
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1645
         * @return mixed Key of first matching element, passed default value or an exception
1646
         */
1647
        public function findKey( \Closure $callback, $default = null, bool $reverse = false )
1648
        {
1649
                $list = $this->list();
15✔
1650

1651
                if( !empty( $list ) )
15✔
1652
                {
1653
                        if( $reverse )
6✔
1654
                        {
1655
                                $value = end( $list );
3✔
1656
                                $key = key( $list );
3✔
1657

1658
                                do
1659
                                {
1660
                                        if( $callback( $value, $key ) ) {
3✔
1661
                                                return $key;
3✔
1662
                                        }
1663
                                }
1664
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
×
1665

1666
                                reset( $list );
×
1667
                        }
1668
                        else
1669
                        {
1670
                                foreach( $list as $key => $value )
3✔
1671
                                {
1672
                                        if( $callback( $value, $key ) ) {
3✔
1673
                                                return $key;
3✔
1674
                                        }
1675
                                }
1676
                        }
1677
                }
1678

1679
                if( $default instanceof \Closure ) {
9✔
1680
                        return $default();
3✔
1681
                }
1682

1683
                if( $default instanceof \Throwable ) {
6✔
1684
                        throw $default;
3✔
1685
                }
1686

1687
                return $default;
3✔
1688
        }
1689

1690

1691
        /**
1692
         * Returns the first element from the map.
1693
         *
1694
         * Examples:
1695
         *  Map::from( ['a', 'b'] )->first();
1696
         *  Map::from( [] )->first( 'x' );
1697
         *  Map::from( [] )->first( new \Exception( 'error' ) );
1698
         *  Map::from( [] )->first( function() { return rand(); } );
1699
         *
1700
         * Results:
1701
         * The first example will return 'b' and the second one 'x'. The third example
1702
         * will throw the exception passed if the map contains no elements. In the
1703
         * fourth example, a random value generated by the closure function will be
1704
         * returned.
1705
         *
1706
         * Using this method doesn't affect the internal array pointer.
1707
         *
1708
         * @param mixed $default Default value, closure or exception if the map contains no elements
1709
         * @return mixed First value of map, (generated) default value or an exception
1710
         */
1711
        public function first( $default = null )
1712
        {
1713
                if( !empty( $this->list() ) ) {
33✔
1714
                        return current( array_slice( $this->list(), 0, 1 ) );
18✔
1715
                }
1716

1717
                if( $default instanceof \Closure ) {
18✔
1718
                        return $default();
3✔
1719
                }
1720

1721
                if( $default instanceof \Throwable ) {
15✔
1722
                        throw $default;
6✔
1723
                }
1724

1725
                return $default;
9✔
1726
        }
1727

1728

1729
        /**
1730
         * Returns the first key from the map.
1731
         *
1732
         * Examples:
1733
         *  Map::from( ['a' => 1, 'b' => 2] )->firstKey();
1734
         *  Map::from( [] )->firstKey( 'x' );
1735
         *  Map::from( [] )->firstKey( new \Exception( 'error' ) );
1736
         *  Map::from( [] )->firstKey( function() { return rand(); } );
1737
         *
1738
         * Results:
1739
         * The first example will return 'a' and the second one 'x', the third one will throw
1740
         * an exception and the last one will return a random value.
1741
         *
1742
         * Using this method doesn't affect the internal array pointer.
1743
         *
1744
         * @param mixed $default Default value, closure or exception if the map contains no elements
1745
         * @return mixed First key of map, (generated) default value or an exception
1746
         */
1747
        public function firstKey( $default = null )
1748
        {
1749
                $list = $this->list();
15✔
1750

1751
                // PHP 7.x compatibility
1752
                if( function_exists( 'array_key_first' ) ) {
15✔
1753
                        $key = array_key_first( $list );
15✔
1754
                } else {
1755
                        $key = key( array_slice( $list, 0, 1, true ) );
×
1756
                }
1757

1758
                if( $key !== null ) {
15✔
1759
                        return $key;
3✔
1760
                }
1761

1762
                if( $default instanceof \Closure ) {
12✔
1763
                        return $default();
3✔
1764
                }
1765

1766
                if( $default instanceof \Throwable ) {
9✔
1767
                        throw $default;
3✔
1768
                }
1769

1770
                return $default;
6✔
1771
        }
1772

1773

1774
        /**
1775
         * Creates a new map with all sub-array elements added recursively without overwriting existing keys.
1776
         *
1777
         * Examples:
1778
         *  Map::from( [[0, 1], [2, 3]] )->flat();
1779
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat();
1780
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat( 1 );
1781
         *  Map::from( [[0, 1], Map::from( [[2, 3], 4] )] )->flat();
1782
         *
1783
         * Results:
1784
         *  [0, 1, 2, 3]
1785
         *  [0, 1, 2, 3, 4]
1786
         *  [0, 1, [2, 3], 4]
1787
         *  [0, 1, 2, 3, 4]
1788
         *
1789
         * The keys are not preserved and the new map elements will be numbered from
1790
         * 0-n. A value smaller than 1 for depth will return the same map elements
1791
         * indexed from 0-n. Flattening does also work if elements implement the
1792
         * "Traversable" interface (which the Map object does).
1793
         *
1794
         * This method is similar than collapse() but doesn't replace existing elements.
1795
         * Keys are NOT preserved using this method!
1796
         *
1797
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1798
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
1799
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1800
         */
1801
        public function flat( ?int $depth = null ) : self
1802
        {
1803
                if( $depth < 0 ) {
18✔
1804
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
3✔
1805
                }
1806

1807
                $result = [];
15✔
1808
                $this->nflatten( $this->list(), $result, $depth ?? 0x7fffffff );
15✔
1809
                return new static( $result );
15✔
1810
        }
1811

1812

1813
        /**
1814
         * Creates a new map with keys joined recursively.
1815
         *
1816
         * Examples:
1817
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->flatten();
1818
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->flatten( 1 );
1819
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->sep( '.' )->flatten();
1820
         *
1821
         * Results:
1822
         *  ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3]
1823
         *  ['a/b' => ['c' => 1, 'd' => 2], 'b/e' => 3]
1824
         *  ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3]
1825
         *
1826
         * To create the original multi-dimensional array again, use the unflatten() method.
1827
         *
1828
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1829
         * @return self<string,mixed> New map with keys joined recursively, up to the specified depth
1830
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1831
         */
1832
        public function flatten( ?int $depth = null ) : self
1833
        {
1834
                if( $depth < 0 ) {
6✔
1835
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
3✔
1836
                }
1837

1838
                $result = [];
3✔
1839
                $this->rflatten( $this->list(), $result, $depth ?? 0x7fffffff );
3✔
1840
                return new static( $result );
3✔
1841
        }
1842

1843

1844
        /**
1845
         * Exchanges the keys with their values and vice versa.
1846
         *
1847
         * Examples:
1848
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->flip();
1849
         *
1850
         * Results:
1851
         *  ['X' => 'a', 'Y' => 'b']
1852
         *
1853
         * @return self<int|string,mixed> New map with keys as values and values as keys
1854
         */
1855
        public function flip() : self
1856
        {
1857
                return new static( array_flip( $this->list() ) );
3✔
1858
        }
1859

1860

1861
        /**
1862
         * Returns an element by key and casts it to float if possible.
1863
         *
1864
         * Examples:
1865
         *  Map::from( ['a' => true] )->float( 'a' );
1866
         *  Map::from( ['a' => 1] )->float( 'a' );
1867
         *  Map::from( ['a' => '1.1'] )->float( 'a' );
1868
         *  Map::from( ['a' => '10'] )->float( 'a' );
1869
         *  Map::from( ['a' => ['b' => ['c' => 1.1]]] )->float( 'a/b/c' );
1870
         *  Map::from( [] )->float( 'c', function() { return 1.1; } );
1871
         *  Map::from( [] )->float( 'a', 1.1 );
1872
         *
1873
         *  Map::from( [] )->float( 'b' );
1874
         *  Map::from( ['b' => ''] )->float( 'b' );
1875
         *  Map::from( ['b' => null] )->float( 'b' );
1876
         *  Map::from( ['b' => 'abc'] )->float( 'b' );
1877
         *  Map::from( ['b' => [1]] )->float( 'b' );
1878
         *  Map::from( ['b' => #resource] )->float( 'b' );
1879
         *  Map::from( ['b' => new \stdClass] )->float( 'b' );
1880
         *
1881
         *  Map::from( [] )->float( 'c', new \Exception( 'error' ) );
1882
         *
1883
         * Results:
1884
         * The first eight examples will return the float values for the passed keys
1885
         * while the 9th to 14th example returns 0. The last example will throw an exception.
1886
         *
1887
         * This does also work for multi-dimensional arrays by passing the keys
1888
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1889
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1890
         * public properties of objects or objects implementing __isset() and __get() methods.
1891
         *
1892
         * @param int|string $key Key or path to the requested item
1893
         * @param mixed $default Default value if key isn't found (will be casted to float)
1894
         * @return float Value from map or default value
1895
         */
1896
        public function float( $key, $default = 0.0 ) : float
1897
        {
1898
                return (float) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
9✔
1899
        }
1900

1901

1902
        /**
1903
         * Returns an element from the map by key.
1904
         *
1905
         * Examples:
1906
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'a' );
1907
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'c', 'Z' );
1908
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->get( 'a/b/c' );
1909
         *  Map::from( [] )->get( 'Y', new \Exception( 'error' ) );
1910
         *  Map::from( [] )->get( 'Y', function() { return rand(); } );
1911
         *
1912
         * Results:
1913
         * The first example will return 'X', the second 'Z' and the third 'Y'. The forth
1914
         * example will throw the exception passed if the map contains no elements. In
1915
         * the fifth example, a random value generated by the closure function will be
1916
         * returned.
1917
         *
1918
         * This does also work for multi-dimensional arrays by passing the keys
1919
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1920
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1921
         * public properties of objects or objects implementing __isset() and __get() methods.
1922
         *
1923
         * @param int|string $key Key or path to the requested item
1924
         * @param mixed $default Default value if no element matches
1925
         * @return mixed Value from map or default value
1926
         */
1927
        public function get( $key, $default = null )
1928
        {
1929
                $list = $this->list();
69✔
1930

1931
                if( array_key_exists( $key, $list ) ) {
69✔
1932
                        return $list[$key];
18✔
1933
                }
1934

1935
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
63✔
1936
                        return $v;
21✔
1937
                }
1938

1939
                if( $default instanceof \Closure ) {
54✔
1940
                        return $default();
18✔
1941
                }
1942

1943
                if( $default instanceof \Throwable ) {
36✔
1944
                        throw $default;
18✔
1945
                }
1946

1947
                return $default;
18✔
1948
        }
1949

1950

1951
        /**
1952
         * Returns an iterator for the elements.
1953
         *
1954
         * This method will be used by e.g. foreach() to loop over all entries:
1955
         *  foreach( Map::from( ['a', 'b'] ) as $value )
1956
         *
1957
         * @return \ArrayIterator<int|string,mixed> Iterator for map elements
1958
         */
1959
        public function getIterator() : \ArrayIterator
1960
        {
1961
                return new \ArrayIterator( $this->list() );
15✔
1962
        }
1963

1964

1965
        /**
1966
         * Returns only items which matches the regular expression.
1967
         *
1968
         * All items are converted to string first before they are compared to the
1969
         * regular expression. Thus, fractions of ".0" will be removed in float numbers
1970
         * which may result in unexpected results.
1971
         *
1972
         * Examples:
1973
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/b/' );
1974
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/a/', PREG_GREP_INVERT );
1975
         *  Map::from( [1.5, 0, 1.0, 'a'] )->grep( '/^(\d+)?\.\d+$/' );
1976
         *
1977
         * Results:
1978
         *  ['ab', 'bc']
1979
         *  ['bc', 'cd']
1980
         *  [1.5] // float 1.0 is converted to string "1"
1981
         *
1982
         * The keys are preserved using this method.
1983
         *
1984
         * @param string $pattern Regular expression pattern, e.g. "/ab/"
1985
         * @param int $flags PREG_GREP_INVERT to return elements not matching the pattern
1986
         * @return self<int|string,mixed> New map containing only the matched elements
1987
         */
1988
        public function grep( string $pattern, int $flags = 0 ) : self
1989
        {
1990
                if( ( $result = preg_grep( $pattern, $this->list(), $flags ) ) === false )
12✔
1991
                {
1992
                        switch( preg_last_error() )
3✔
1993
                        {
1994
                                case PREG_INTERNAL_ERROR: $msg = 'Internal error'; break;
3✔
1995
                                case PREG_BACKTRACK_LIMIT_ERROR: $msg = 'Backtrack limit error'; break;
×
1996
                                case PREG_RECURSION_LIMIT_ERROR: $msg = 'Recursion limit error'; break;
×
1997
                                case PREG_BAD_UTF8_ERROR: $msg = 'Bad UTF8 error'; break;
×
1998
                                case PREG_BAD_UTF8_OFFSET_ERROR: $msg = 'Bad UTF8 offset error'; break;
×
1999
                                case PREG_JIT_STACKLIMIT_ERROR: $msg = 'JIT stack limit error'; break;
×
2000
                                default: $msg = 'Unknown error';
×
2001
                        }
2002

2003
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
3✔
2004
                }
2005

2006
                return new static( $result );
9✔
2007
        }
2008

2009

2010
        /**
2011
         * Groups associative array elements or objects by the passed key or closure.
2012
         *
2013
         * Instead of overwriting items with the same keys like to the col() method
2014
         * does, groupBy() keeps all entries in sub-arrays. It's preserves the keys
2015
         * of the orignal map entries too.
2016
         *
2017
         * Examples:
2018
         *  $list = [
2019
         *    10 => ['aid' => 123, 'code' => 'x-abc'],
2020
         *    20 => ['aid' => 123, 'code' => 'x-def'],
2021
         *    30 => ['aid' => 456, 'code' => 'x-def']
2022
         *  ];
2023
         *  Map::from( $list )->groupBy( 'aid' );
2024
         *  Map::from( $list )->groupBy( function( $item, $key ) {
2025
         *    return substr( $item['code'], -3 );
2026
         *  } );
2027
         *  Map::from( $list )->groupBy( 'xid' );
2028
         *
2029
         * Results:
2030
         *  [
2031
         *    123 => [10 => ['aid' => 123, 'code' => 'x-abc'], 20 => ['aid' => 123, 'code' => 'x-def']],
2032
         *    456 => [30 => ['aid' => 456, 'code' => 'x-def']]
2033
         *  ]
2034
         *  [
2035
         *    'abc' => [10 => ['aid' => 123, 'code' => 'x-abc']],
2036
         *    'def' => [20 => ['aid' => 123, 'code' => 'x-def'], 30 => ['aid' => 456, 'code' => 'x-def']]
2037
         *  ]
2038
         *  [
2039
         *    '' => [
2040
         *      10 => ['aid' => 123, 'code' => 'x-abc'],
2041
         *      20 => ['aid' => 123, 'code' => 'x-def'],
2042
         *      30 => ['aid' => 456, 'code' => 'x-def']
2043
         *    ]
2044
         *  ]
2045
         *
2046
         * In case the passed key doesn't exist in one or more items, these items
2047
         * are stored in a sub-array using an empty string as key.
2048
         *
2049
         * This does also work for multi-dimensional arrays by passing the keys
2050
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2051
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2052
         * public properties of objects or objects implementing __isset() and __get() methods.
2053
         *
2054
         * @param  \Closure|string|int $key Closure function with (item, idx) parameters returning the key or the key itself to group by
2055
         * @return self<int|string,mixed> New map with elements grouped by the given key
2056
         */
2057
        public function groupBy( $key ) : self
2058
        {
2059
                $result = [];
12✔
2060

2061
                if( is_callable( $key ) )
12✔
2062
                {
2063
                        foreach( $this->list() as $idx => $item )
3✔
2064
                        {
2065
                                $keyval = (string) $key( $item, $idx );
3✔
2066
                                $result[$keyval][$idx] = $item;
3✔
2067
                        }
2068
                }
2069
                else
2070
                {
2071
                        $parts = explode( $this->sep, (string) $key );
9✔
2072

2073
                        foreach( $this->list() as $idx => $item )
9✔
2074
                        {
2075
                                $keyval = (string) $this->val( $item, $parts );
9✔
2076
                                $result[$keyval][$idx] = $item;
9✔
2077
                        }
2078
                }
2079

2080
                return new static( $result );
12✔
2081
        }
2082

2083

2084
        /**
2085
         * Determines if a key or several keys exists in the map.
2086
         *
2087
         * If several keys are passed as array, all keys must exist in the map for
2088
         * TRUE to be returned.
2089
         *
2090
         * Examples:
2091
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'a' );
2092
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'b'] );
2093
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->has( 'a/b/c' );
2094
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'c' );
2095
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'c'] );
2096
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'X' );
2097
         *
2098
         * Results:
2099
         * The first three examples will return TRUE while the other ones will return FALSE
2100
         *
2101
         * This does also work for multi-dimensional arrays by passing the keys
2102
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2103
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2104
         * public properties of objects or objects implementing __isset() and __get() methods.
2105
         *
2106
         * @param array<int|string>|int|string $key Key of the requested item or list of keys
2107
         * @return bool TRUE if key or keys are available in map, FALSE if not
2108
         */
2109
        public function has( $key ) : bool
2110
        {
2111
                $list = $this->list();
9✔
2112

2113
                foreach( (array) $key as $entry )
9✔
2114
                {
2115
                        if( array_key_exists( $entry, $list ) === false
9✔
2116
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
9✔
2117
                        ) {
2118
                                return false;
9✔
2119
                        }
2120
                }
2121

2122
                return true;
9✔
2123
        }
2124

2125

2126
        /**
2127
         * Executes callbacks depending on the condition.
2128
         *
2129
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2130
         * executed and their returned value is passed back within a Map object. In
2131
         * case no "then" or "else" closure is given, the method will return the same
2132
         * map object.
2133
         *
2134
         * Examples:
2135
         *  Map::from( [] )->if( strpos( 'abc', 'b' ) !== false, function( $map ) {
2136
         *    echo 'found';
2137
         *  } );
2138
         *
2139
         *  Map::from( [] )->if( function( $map ) {
2140
         *    return $map->empty();
2141
         *  }, function( $map ) {
2142
         *    echo 'then';
2143
         *  } );
2144
         *
2145
         *  Map::from( ['a'] )->if( function( $map ) {
2146
         *    return $map->empty();
2147
         *  }, function( $map ) {
2148
         *    echo 'then';
2149
         *  }, function( $map ) {
2150
         *    echo 'else';
2151
         *  } );
2152
         *
2153
         *  Map::from( ['a', 'b'] )->if( true, function( $map ) {
2154
         *    return $map->push( 'c' );
2155
         *  } );
2156
         *
2157
         *  Map::from( ['a', 'b'] )->if( false, null, function( $map ) {
2158
         *    return $map->pop();
2159
         *  } );
2160
         *
2161
         * Results:
2162
         * The first example returns "found" while the second one returns "then" and
2163
         * the third one "else". The forth one will return ['a', 'b', 'c'] while the
2164
         * fifth one will return 'b', which is turned into a map of ['b'] again.
2165
         *
2166
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2167
         * (a short form for anonymous closures) as parameters. They automatically have access
2168
         * to previously defined variables but can not modify them. Also, they can not have
2169
         * a void return type and must/will always return something. Details about
2170
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2171
         *
2172
         * @param \Closure|bool $condition Boolean or function with (map) parameter returning a boolean
2173
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2174
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2175
         * @return self<int|string,mixed> New map
2176
         */
2177
        public function if( $condition, ?\Closure $then = null, ?\Closure $else = null ) : self
2178
        {
2179
                if( $condition instanceof \Closure ) {
24✔
2180
                        $condition = $condition( $this );
6✔
2181
                }
2182

2183
                if( $condition ) {
24✔
2184
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
15✔
2185
                } elseif( $else ) {
9✔
2186
                        return static::from( $else( $this, $condition ) );
9✔
2187
                }
2188

2189
                return $this;
×
2190
        }
2191

2192

2193
        /**
2194
         * Executes callbacks depending if the map contains elements or not.
2195
         *
2196
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2197
         * executed and their returned value is passed back within a Map object. In
2198
         * case no "then" or "else" closure is given, the method will return the same
2199
         * map object.
2200
         *
2201
         * Examples:
2202
         *  Map::from( ['a'] )->ifAny( function( $map ) {
2203
         *    $map->push( 'b' );
2204
         *  } );
2205
         *
2206
         *  Map::from( [] )->ifAny( null, function( $map ) {
2207
         *    return $map->push( 'b' );
2208
         *  } );
2209
         *
2210
         *  Map::from( ['a'] )->ifAny( function( $map ) {
2211
         *    return 'c';
2212
         *  } );
2213
         *
2214
         * Results:
2215
         * The first example returns a Map containing ['a', 'b'] because the initial
2216
         * Map is not empty. The second one returns a Map with ['b'] because the initial
2217
         * Map is empty and the "else" closure is used. The last example returns ['c']
2218
         * as new map content.
2219
         *
2220
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2221
         * (a short form for anonymous closures) as parameters. They automatically have access
2222
         * to previously defined variables but can not modify them. Also, they can not have
2223
         * a void return type and must/will always return something. Details about
2224
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2225
         *
2226
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2227
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2228
         * @return self<int|string,mixed> New map
2229
         */
2230
        public function ifAny( ?\Closure $then = null, ?\Closure $else = null ) : self
2231
        {
2232
                return $this->if( !empty( $this->list() ), $then, $else );
9✔
2233
        }
2234

2235

2236
        /**
2237
         * Executes callbacks depending if the map is empty or not.
2238
         *
2239
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2240
         * executed and their returned value is passed back within a Map object. In
2241
         * case no "then" or "else" closure is given, the method will return the same
2242
         * map object.
2243
         *
2244
         * Examples:
2245
         *  Map::from( [] )->ifEmpty( function( $map ) {
2246
         *    $map->push( 'a' );
2247
         *  } );
2248
         *
2249
         *  Map::from( ['a'] )->ifEmpty( null, function( $map ) {
2250
         *    return $map->push( 'b' );
2251
         *  } );
2252
         *
2253
         * Results:
2254
         * The first example returns a Map containing ['a'] because the initial Map
2255
         * is empty. The second one returns a Map with ['a', 'b'] because the initial
2256
         * Map is not empty and the "else" closure is used.
2257
         *
2258
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2259
         * (a short form for anonymous closures) as parameters. They automatically have access
2260
         * to previously defined variables but can not modify them. Also, they can not have
2261
         * a void return type and must/will always return something. Details about
2262
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2263
         *
2264
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2265
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2266
         * @return self<int|string,mixed> New map
2267
         */
2268
        public function ifEmpty( ?\Closure $then = null, ?\Closure $else = null ) : self
2269
        {
2270
                return $this->if( empty( $this->list() ), $then, $else );
×
2271
        }
2272

2273

2274
        /**
2275
         * Tests if all entries in the map are objects implementing the given interface.
2276
         *
2277
         * Examples:
2278
         *  Map::from( [new Map(), new Map()] )->implements( '\Countable' );
2279
         *  Map::from( [new Map(), new stdClass()] )->implements( '\Countable' );
2280
         *  Map::from( [new Map(), 123] )->implements( '\Countable' );
2281
         *  Map::from( [new Map(), 123] )->implements( '\Countable', true );
2282
         *  Map::from( [new Map(), 123] )->implements( '\Countable', '\RuntimeException' );
2283
         *
2284
         * Results:
2285
         *  The first example returns TRUE while the second and third one return FALSE.
2286
         *  The forth example will throw an UnexpectedValueException while the last one
2287
         *  throws a RuntimeException.
2288
         *
2289
         * @param string $interface Name of the interface that must be implemented
2290
         * @param \Throwable|bool $throw Passing TRUE or an exception name will throw the exception instead of returning FALSE
2291
         * @return bool TRUE if all entries implement the interface or FALSE if at least one doesn't
2292
         * @throws \UnexpectedValueException|\Throwable If one entry doesn't implement the interface
2293
         */
2294
        public function implements( string $interface, $throw = false ) : bool
2295
        {
2296
                foreach( $this->list() as $entry )
9✔
2297
                {
2298
                        if( !( $entry instanceof $interface ) )
9✔
2299
                        {
2300
                                if( $throw )
9✔
2301
                                {
2302
                                        $name = is_string( $throw ) ? $throw : '\UnexpectedValueException';
6✔
2303
                                        throw new $name( "Does not implement $interface: " . print_r( $entry, true ) );
6✔
2304
                                }
2305

2306
                                return false;
3✔
2307
                        }
2308
                }
2309

2310
                return true;
3✔
2311
        }
2312

2313

2314
        /**
2315
         * Tests if the passed element or elements are part of the map.
2316
         *
2317
         * Examples:
2318
         *  Map::from( ['a', 'b'] )->in( 'a' );
2319
         *  Map::from( ['a', 'b'] )->in( ['a', 'b'] );
2320
         *  Map::from( ['a', 'b'] )->in( 'x' );
2321
         *  Map::from( ['a', 'b'] )->in( ['a', 'x'] );
2322
         *  Map::from( ['1', '2'] )->in( 2, true );
2323
         *
2324
         * Results:
2325
         * The first and second example will return TRUE while the other ones will return FALSE
2326
         *
2327
         * @param mixed|array $element Element or elements to search for in the map
2328
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2329
         * @return bool TRUE if all elements are available in map, FALSE if not
2330
         */
2331
        public function in( $element, bool $strict = false ) : bool
2332
        {
2333
                if( !is_array( $element ) ) {
12✔
2334
                        return in_array( $element, $this->list(), $strict );
12✔
2335
                };
2336

2337
                foreach( $element as $entry )
3✔
2338
                {
2339
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
3✔
2340
                                return false;
3✔
2341
                        }
2342
                }
2343

2344
                return true;
3✔
2345
        }
2346

2347

2348
        /**
2349
         * Tests if the passed element or elements are part of the map.
2350
         *
2351
         * This method is an alias for in(). For performance reasons, in() should be
2352
         * preferred because it uses one method call less than includes().
2353
         *
2354
         * @param mixed|array $element Element or elements to search for in the map
2355
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2356
         * @return bool TRUE if all elements are available in map, FALSE if not
2357
         * @see in() - Underlying method with same parameters and return value but better performance
2358
         */
2359
        public function includes( $element, bool $strict = false ) : bool
2360
        {
2361
                return $this->in( $element, $strict );
3✔
2362
        }
2363

2364

2365
        /**
2366
         * Returns the numerical index of the given key.
2367
         *
2368
         * Examples:
2369
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( '8' );
2370
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( function( $key ) {
2371
         *      return $key == '8';
2372
         *  } );
2373
         *
2374
         * Results:
2375
         * Both examples will return "1" because the value "b" is at the second position
2376
         * and the returned index is zero based so the first item has the index "0".
2377
         *
2378
         * @param \Closure|string|int $value Key to search for or function with (key) parameters return TRUE if key is found
2379
         * @return int|null Position of the found value (zero based) or NULL if not found
2380
         */
2381
        public function index( $value ) : ?int
2382
        {
2383
                if( $value instanceof \Closure )
12✔
2384
                {
2385
                        $pos = 0;
6✔
2386

2387
                        foreach( $this->list() as $key => $item )
6✔
2388
                        {
2389
                                if( $value( $key ) ) {
3✔
2390
                                        return $pos;
3✔
2391
                                }
2392

2393
                                ++$pos;
3✔
2394
                        }
2395

2396
                        return null;
3✔
2397
                }
2398

2399
                $pos = array_search( $value, array_keys( $this->list() ) );
6✔
2400
                return $pos !== false ? $pos : null;
6✔
2401
        }
2402

2403

2404
        /**
2405
         * Inserts the value or values after the given element.
2406
         *
2407
         * Examples:
2408
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAfter( 'foo', 'baz' );
2409
         *  Map::from( ['foo', 'bar'] )->insertAfter( 'foo', ['baz', 'boo'] );
2410
         *  Map::from( ['foo', 'bar'] )->insertAfter( null, 'baz' );
2411
         *
2412
         * Results:
2413
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2414
         *  ['foo', 'baz', 'boo', 'bar']
2415
         *  ['foo', 'bar', 'baz']
2416
         *
2417
         * Numerical array indexes are not preserved.
2418
         *
2419
         * @param mixed $element Element after the value is inserted
2420
         * @param mixed $value Element or list of elements to insert
2421
         * @return self<int|string,mixed> Updated map for fluid interface
2422
         */
2423
        public function insertAfter( $element, $value ) : self
2424
        {
2425
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
9✔
2426
                array_splice( $this->list(), $position + 1, 0, $this->array( $value ) );
9✔
2427

2428
                return $this;
9✔
2429
        }
2430

2431

2432
        /**
2433
         * Inserts the item at the given position in the map.
2434
         *
2435
         * Examples:
2436
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 0, 'baz' );
2437
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 1, 'baz', 'c' );
2438
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 4, 'baz' );
2439
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( -1, 'baz', 'c' );
2440
         *
2441
         * Results:
2442
         *  [0 => 'baz', 'a' => 'foo', 'b' => 'bar']
2443
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2444
         *  ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']
2445
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2446
         *
2447
         * @param int $pos Position the value should be inserted at
2448
         * @param mixed $value Value to be inserted
2449
         * @param mixed|null $key Value key or NULL to assign an integer key automatically
2450
         * @return self<int|string,mixed> Updated map for fluid interface
2451
         */
2452
        public function insertAt( int $pos, $value, $key = null ) : self
2453
        {
2454
                if( $key !== null )
15✔
2455
                {
2456
                        $list = $this->list();
6✔
2457

2458
                        $this->list = array_merge(
6✔
2459
                                array_slice( $list, 0, $pos, true ),
6✔
2460
                                [$key => $value],
6✔
2461
                                array_slice( $list, $pos, null, true )
6✔
2462
                        );
6✔
2463
                }
2464
                else
2465
                {
2466
                        array_splice( $this->list(), $pos, 0, [$value] );
9✔
2467
                }
2468

2469
                return $this;
15✔
2470
        }
2471

2472

2473
        /**
2474
         * Inserts the value or values before the given element.
2475
         *
2476
         * Examples:
2477
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertBefore( 'bar', 'baz' );
2478
         *  Map::from( ['foo', 'bar'] )->insertBefore( 'bar', ['baz', 'boo'] );
2479
         *  Map::from( ['foo', 'bar'] )->insertBefore( null, 'baz' );
2480
         *
2481
         * Results:
2482
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2483
         *  ['foo', 'baz', 'boo', 'bar']
2484
         *  ['foo', 'bar', 'baz']
2485
         *
2486
         * Numerical array indexes are not preserved.
2487
         *
2488
         * @param mixed $element Element before the value is inserted
2489
         * @param mixed $value Element or list of elements to insert
2490
         * @return self<int|string,mixed> Updated map for fluid interface
2491
         */
2492
        public function insertBefore( $element, $value ) : self
2493
        {
2494
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
9✔
2495
                array_splice( $this->list(), $position, 0, $this->array( $value ) );
9✔
2496

2497
                return $this;
9✔
2498
        }
2499

2500

2501
        /**
2502
         * Tests if the passed value or values are part of the strings in the map.
2503
         *
2504
         * Examples:
2505
         *  Map::from( ['abc'] )->inString( 'c' );
2506
         *  Map::from( ['abc'] )->inString( 'bc' );
2507
         *  Map::from( [12345] )->inString( '23' );
2508
         *  Map::from( [123.4] )->inString( 23.4 );
2509
         *  Map::from( [12345] )->inString( false );
2510
         *  Map::from( [12345] )->inString( true );
2511
         *  Map::from( [false] )->inString( false );
2512
         *  Map::from( ['abc'] )->inString( '' );
2513
         *  Map::from( [''] )->inString( false );
2514
         *  Map::from( ['abc'] )->inString( 'BC', false );
2515
         *  Map::from( ['abc', 'def'] )->inString( ['de', 'xy'] );
2516
         *  Map::from( ['abc', 'def'] )->inString( ['E', 'x'] );
2517
         *  Map::from( ['abc', 'def'] )->inString( 'E' );
2518
         *  Map::from( [23456] )->inString( true );
2519
         *  Map::from( [false] )->inString( 0 );
2520
         *
2521
         * Results:
2522
         * The first eleven examples will return TRUE while the last four will return FALSE
2523
         *
2524
         * All scalar values (bool, float, int and string) are casted to string values before
2525
         * comparing to the given value. Non-scalar values in the map are ignored.
2526
         *
2527
         * @param array|string $value Value or values to compare the map elements, will be casted to string type
2528
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
2529
         * @return bool TRUE If at least one element matches, FALSE if value is not in any string of the map
2530
         * @deprecated Use multi-byte aware strContains() instead
2531
         */
2532
        public function inString( $value, bool $case = true ) : bool
2533
        {
2534
                $fcn = $case ? 'strpos' : 'stripos';
3✔
2535

2536
                foreach( (array) $value as $val )
3✔
2537
                {
2538
                        if( (string) $val === '' ) {
3✔
2539
                                return true;
3✔
2540
                        }
2541

2542
                        foreach( $this->list() as $item )
3✔
2543
                        {
2544
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
3✔
2545
                                        return true;
3✔
2546
                                }
2547
                        }
2548
                }
2549

2550
                return false;
3✔
2551
        }
2552

2553

2554
        /**
2555
         * Returns an element by key and casts it to integer if possible.
2556
         *
2557
         * Examples:
2558
         *  Map::from( ['a' => true] )->int( 'a' );
2559
         *  Map::from( ['a' => '1'] )->int( 'a' );
2560
         *  Map::from( ['a' => 1.1] )->int( 'a' );
2561
         *  Map::from( ['a' => '10'] )->int( 'a' );
2562
         *  Map::from( ['a' => ['b' => ['c' => 1]]] )->int( 'a/b/c' );
2563
         *  Map::from( [] )->int( 'c', function() { return rand( 1, 1 ); } );
2564
         *  Map::from( [] )->int( 'a', 1 );
2565
         *
2566
         *  Map::from( [] )->int( 'b' );
2567
         *  Map::from( ['b' => ''] )->int( 'b' );
2568
         *  Map::from( ['b' => 'abc'] )->int( 'b' );
2569
         *  Map::from( ['b' => null] )->int( 'b' );
2570
         *  Map::from( ['b' => [1]] )->int( 'b' );
2571
         *  Map::from( ['b' => #resource] )->int( 'b' );
2572
         *  Map::from( ['b' => new \stdClass] )->int( 'b' );
2573
         *
2574
         *  Map::from( [] )->int( 'c', new \Exception( 'error' ) );
2575
         *
2576
         * Results:
2577
         * The first seven examples will return 1 while the 8th to 14th example
2578
         * returns 0. The last example will throw an exception.
2579
         *
2580
         * This does also work for multi-dimensional arrays by passing the keys
2581
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2582
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2583
         * public properties of objects or objects implementing __isset() and __get() methods.
2584
         *
2585
         * @param int|string $key Key or path to the requested item
2586
         * @param mixed $default Default value if key isn't found (will be casted to integer)
2587
         * @return int Value from map or default value
2588
         */
2589
        public function int( $key, $default = 0 ) : int
2590
        {
2591
                return (int) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
9✔
2592
        }
2593

2594

2595
        /**
2596
         * Returns all values in a new map that are available in both, the map and the given elements.
2597
         *
2598
         * Examples:
2599
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersect( ['bar'] );
2600
         *
2601
         * Results:
2602
         *  ['b' => 'bar']
2603
         *
2604
         * If a callback is passed, the given function will be used to compare the values.
2605
         * The function must accept two parameters (value A and B) and must return
2606
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2607
         * greater than value B. Both, a method name and an anonymous function can be passed:
2608
         *
2609
         *  Map::from( [0 => 'a'] )->intersect( [0 => 'A'], 'strcasecmp' );
2610
         *  Map::from( ['b' => 'a'] )->intersect( ['B' => 'A'], 'strcasecmp' );
2611
         *  Map::from( ['b' => 'a'] )->intersect( ['c' => 'A'], function( $valA, $valB ) {
2612
         *      return strtolower( $valA ) <=> strtolower( $valB );
2613
         *  } );
2614
         *
2615
         * All examples will return a map containing ['a'] because both contain the same
2616
         * values when compared case insensitive.
2617
         *
2618
         * The keys are preserved using this method.
2619
         *
2620
         * @param iterable<int|string,mixed> $elements List of elements
2621
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2622
         * @return self<int|string,mixed> New map
2623
         */
2624
        public function intersect( iterable $elements, ?callable $callback = null ) : self
2625
        {
2626
                $list = $this->list();
6✔
2627
                $elements = $this->array( $elements );
6✔
2628

2629
                if( $callback ) {
6✔
2630
                        return new static( array_uintersect( $list, $elements, $callback ) );
3✔
2631
                }
2632

2633
                return new static( array_intersect( $list, $elements ) );
3✔
2634
        }
2635

2636

2637
        /**
2638
         * Returns all values in a new map that are available in both, the map and the given elements while comparing the keys too.
2639
         *
2640
         * Examples:
2641
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectAssoc( new Map( ['foo', 'b' => 'bar'] ) );
2642
         *
2643
         * Results:
2644
         *  ['a' => 'foo']
2645
         *
2646
         * If a callback is passed, the given function will be used to compare the values.
2647
         * The function must accept two parameters (value A and B) and must return
2648
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2649
         * greater than value B. Both, a method name and an anonymous function can be passed:
2650
         *
2651
         *  Map::from( [0 => 'a'] )->intersectAssoc( [0 => 'A'], 'strcasecmp' );
2652
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['B' => 'A'], 'strcasecmp' );
2653
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['c' => 'A'], function( $valA, $valB ) {
2654
         *      return strtolower( $valA ) <=> strtolower( $valB );
2655
         *  } );
2656
         *
2657
         * The first example will return [0 => 'a'] because both contain the same
2658
         * values when compared case insensitive. The second and third example will return
2659
         * an empty map because the keys doesn't match ("b" vs. "B" and "b" vs. "c").
2660
         *
2661
         * The keys are preserved using this method.
2662
         *
2663
         * @param iterable<int|string,mixed> $elements List of elements
2664
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2665
         * @return self<int|string,mixed> New map
2666
         */
2667
        public function intersectAssoc( iterable $elements, ?callable $callback = null ) : self
2668
        {
2669
                $elements = $this->array( $elements );
15✔
2670

2671
                if( $callback ) {
15✔
2672
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
3✔
2673
                }
2674

2675
                return new static( array_intersect_assoc( $this->list(), $elements ) );
12✔
2676
        }
2677

2678

2679
        /**
2680
         * Returns all values in a new map that are available in both, the map and the given elements by comparing the keys only.
2681
         *
2682
         * Examples:
2683
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectKeys( new Map( ['foo', 'b' => 'baz'] ) );
2684
         *
2685
         * Results:
2686
         *  ['b' => 'bar']
2687
         *
2688
         * If a callback is passed, the given function will be used to compare the keys.
2689
         * The function must accept two parameters (key A and B) and must return
2690
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
2691
         * greater than key B. Both, a method name and an anonymous function can be passed:
2692
         *
2693
         *  Map::from( [0 => 'a'] )->intersectKeys( [0 => 'A'], 'strcasecmp' );
2694
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['B' => 'X'], 'strcasecmp' );
2695
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['c' => 'a'], function( $keyA, $keyB ) {
2696
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
2697
         *  } );
2698
         *
2699
         * The first example will return a map with [0 => 'a'] and the second one will
2700
         * return a map with ['b' => 'a'] because both contain the same keys when compared
2701
         * case insensitive. The third example will return an empty map because the keys
2702
         * doesn't match ("b" vs. "c").
2703
         *
2704
         * The keys are preserved using this method.
2705
         *
2706
         * @param iterable<int|string,mixed> $elements List of elements
2707
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
2708
         * @return self<int|string,mixed> New map
2709
         */
2710
        public function intersectKeys( iterable $elements, ?callable $callback = null ) : self
2711
        {
2712
                $elements = $this->array( $elements );
9✔
2713

2714
                if( $callback ) {
9✔
2715
                        return new static( array_intersect_ukey( $this->list(), $elements, $callback ) );
3✔
2716
                }
2717

2718
                return new static( array_intersect_key( $this->list(), $elements ) );
6✔
2719
        }
2720

2721

2722
        /**
2723
         * Tests if the map consists of the same keys and values
2724
         *
2725
         * Examples:
2726
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'] );
2727
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'], true );
2728
         *  Map::from( [1, 2] )->is( ['1', '2'] );
2729
         *
2730
         * Results:
2731
         *  The first example returns TRUE while the second and third one returns FALSE
2732
         *
2733
         * @param iterable<int|string,mixed> $list List of key/value pairs to compare with
2734
         * @param bool $strict TRUE for comparing order of elements too, FALSE for key/values only
2735
         * @return bool TRUE if given list is equal, FALSE if not
2736
         */
2737
        public function is( iterable $list, bool $strict = false ) : bool
2738
        {
2739
                $list = $this->array( $list );
9✔
2740

2741
                if( $strict ) {
9✔
2742
                        return $this->list() === $list;
6✔
2743
                }
2744

2745
                return $this->list() == $list;
3✔
2746
        }
2747

2748

2749
        /**
2750
         * Determines if the map is empty or not.
2751
         *
2752
         * Examples:
2753
         *  Map::from( [] )->isEmpty();
2754
         *  Map::from( ['a'] )->isEmpty();
2755
         *
2756
         * Results:
2757
         *  The first example returns TRUE while the second returns FALSE
2758
         *
2759
         * The method is equivalent to empty().
2760
         *
2761
         * @return bool TRUE if map is empty, FALSE if not
2762
         */
2763
        public function isEmpty() : bool
2764
        {
2765
                return empty( $this->list() );
12✔
2766
        }
2767

2768

2769
        /**
2770
         * Checks if the map contains a list of subsequentially numbered keys.
2771
         *
2772
         * Examples:
2773
         * Map::from( [] )->isList();
2774
         * Map::from( [1, 3, 2] )->isList();
2775
         * Map::from( [0 => 1, 1 => 2, 2 => 3] )->isList();
2776
         * Map::from( [1 => 1, 2 => 2, 3 => 3] )->isList();
2777
         * Map::from( [0 => 1, 2 => 2, 3 => 3] )->isList();
2778
         * Map::from( ['a' => 1, 1 => 2, 'c' => 3] )->isList();
2779
         *
2780
         * Results:
2781
         * The first three examples return TRUE while the last three return FALSE
2782
         *
2783
         * @return bool TRUE if the map is a list, FALSE if not
2784
         */
2785
        public function isList() : bool
2786
        {
2787
                $i = -1;
3✔
2788

2789
                foreach( $this->list() as $k => $v )
3✔
2790
                {
2791
                        if( $k !== ++$i ) {
3✔
2792
                                return false;
3✔
2793
                        }
2794
                }
2795

2796
                return true;
3✔
2797
        }
2798

2799

2800
        /**
2801
         * Determines if all entries are numeric values.
2802
         *
2803
         * Examples:
2804
         *  Map::from( [] )->isNumeric();
2805
         *  Map::from( [1] )->isNumeric();
2806
         *  Map::from( [1.1] )->isNumeric();
2807
         *  Map::from( [010] )->isNumeric();
2808
         *  Map::from( [0x10] )->isNumeric();
2809
         *  Map::from( [0b10] )->isNumeric();
2810
         *  Map::from( ['010'] )->isNumeric();
2811
         *  Map::from( ['10'] )->isNumeric();
2812
         *  Map::from( ['10.1'] )->isNumeric();
2813
         *  Map::from( [' 10 '] )->isNumeric();
2814
         *  Map::from( ['10e2'] )->isNumeric();
2815
         *  Map::from( ['0b10'] )->isNumeric();
2816
         *  Map::from( ['0x10'] )->isNumeric();
2817
         *  Map::from( ['null'] )->isNumeric();
2818
         *  Map::from( [null] )->isNumeric();
2819
         *  Map::from( [true] )->isNumeric();
2820
         *  Map::from( [[]] )->isNumeric();
2821
         *  Map::from( [''] )->isNumeric();
2822
         *
2823
         * Results:
2824
         *  The first eleven examples return TRUE while the last seven return FALSE
2825
         *
2826
         * @return bool TRUE if all map entries are numeric values, FALSE if not
2827
         */
2828
        public function isNumeric() : bool
2829
        {
2830
                foreach( $this->list() as $val )
3✔
2831
                {
2832
                        if( !is_numeric( $val ) ) {
3✔
2833
                                return false;
3✔
2834
                        }
2835
                }
2836

2837
                return true;
3✔
2838
        }
2839

2840

2841
        /**
2842
         * Determines if all entries are objects.
2843
         *
2844
         * Examples:
2845
         *  Map::from( [] )->isObject();
2846
         *  Map::from( [new stdClass] )->isObject();
2847
         *  Map::from( [1] )->isObject();
2848
         *
2849
         * Results:
2850
         *  The first two examples return TRUE while the last one return FALSE
2851
         *
2852
         * @return bool TRUE if all map entries are objects, FALSE if not
2853
         */
2854
        public function isObject() : bool
2855
        {
2856
                foreach( $this->list() as $val )
3✔
2857
                {
2858
                        if( !is_object( $val ) ) {
3✔
2859
                                return false;
3✔
2860
                        }
2861
                }
2862

2863
                return true;
3✔
2864
        }
2865

2866

2867
        /**
2868
         * Determines if all entries are scalar values.
2869
         *
2870
         * Examples:
2871
         *  Map::from( [] )->isScalar();
2872
         *  Map::from( [1] )->isScalar();
2873
         *  Map::from( [1.1] )->isScalar();
2874
         *  Map::from( ['abc'] )->isScalar();
2875
         *  Map::from( [true, false] )->isScalar();
2876
         *  Map::from( [new stdClass] )->isScalar();
2877
         *  Map::from( [#resource] )->isScalar();
2878
         *  Map::from( [null] )->isScalar();
2879
         *  Map::from( [[1]] )->isScalar();
2880
         *
2881
         * Results:
2882
         *  The first five examples return TRUE while the others return FALSE
2883
         *
2884
         * @return bool TRUE if all map entries are scalar values, FALSE if not
2885
         */
2886
        public function isScalar() : bool
2887
        {
2888
                foreach( $this->list() as $val )
3✔
2889
                {
2890
                        if( !is_scalar( $val ) ) {
3✔
2891
                                return false;
3✔
2892
                        }
2893
                }
2894

2895
                return true;
3✔
2896
        }
2897

2898

2899
        /**
2900
         * Tests for the matching item, but is true only if exactly one item is matching.
2901
         *
2902
         * Examples:
2903
         *  Map::from( ['a', 'b'] )->isSole( 'a' );
2904
         *  Map::from( ['a', 'b', 'a'] )->isSole( fn( $v, $k ) => $v === 'a' && $k < 2 );
2905
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->isSole( fn( $v, $k ) => $v['name'] === 'user' );
2906
         *  Map::from( ['b', 'c'] )->isSole( 'a' );
2907
         *  Map::from( ['a', 'b', 'a'] )->isSole( 'a' );
2908
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->isSole( 'test', 'name' );
2909
         *
2910
         * Results:
2911
         * The first three examples will return TRUE while all others will return FALSE.
2912
         *
2913
         * @param \Closure|mixed $values Closure with (item, key) parameter or element to test against
2914
         * @param string|int|null $key Key to compare the value for if $values is not a closure
2915
         * @return bool TRUE if exactly one item matches, FALSE if no or more than one item matches
2916
         */
2917
        public function isSole( $value = null, $key = null ) : bool
2918
        {
2919
                return $this->restrict( $value, $key )->count() === 1;
9✔
2920
        }
2921

2922

2923
        /**
2924
         * Determines if all entries are string values.
2925
         *
2926
         * Examples:
2927
         *  Map::from( ['abc'] )->isString();
2928
         *  Map::from( [] )->isString();
2929
         *  Map::from( [1] )->isString();
2930
         *  Map::from( [1.1] )->isString();
2931
         *  Map::from( [true, false] )->isString();
2932
         *  Map::from( [new stdClass] )->isString();
2933
         *  Map::from( [#resource] )->isString();
2934
         *  Map::from( [null] )->isString();
2935
         *  Map::from( [[1]] )->isString();
2936
         *
2937
         * Results:
2938
         *  The first two examples return TRUE while the others return FALSE
2939
         *
2940
         * @return bool TRUE if all map entries are string values, FALSE if not
2941
         */
2942
        public function isString() : bool
2943
        {
2944
                foreach( $this->list() as $val )
3✔
2945
                {
2946
                        if( !is_string( $val ) ) {
3✔
2947
                                return false;
3✔
2948
                        }
2949
                }
2950

2951
                return true;
3✔
2952
        }
2953

2954

2955
        /**
2956
         * Concatenates the string representation of all elements.
2957
         *
2958
         * Objects that implement __toString() does also work, otherwise (and in case
2959
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2960
         * empty strings.
2961
         *
2962
         * Examples:
2963
         *  Map::from( ['a', 'b', false] )->join();
2964
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
2965
         *
2966
         * Results:
2967
         * The first example will return "ab" while the second one will return "a-b--"
2968
         *
2969
         * @param string $glue Character or string added between elements
2970
         * @return string String of concatenated map elements
2971
         */
2972
        public function join( string $glue = '' ) : string
2973
        {
2974
                return implode( $glue, $this->list() );
3✔
2975
        }
2976

2977

2978
        /**
2979
         * Specifies the data which should be serialized to JSON by json_encode().
2980
         *
2981
         * Examples:
2982
         *   json_encode( Map::from( ['a', 'b'] ) );
2983
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
2984
         *
2985
         * Results:
2986
         *   ["a", "b"]
2987
         *   {"a":0,"b":1}
2988
         *
2989
         * @return array<int|string,mixed> Data to serialize to JSON
2990
         */
2991
        #[\ReturnTypeWillChange]
2992
        public function jsonSerialize()
2993
        {
2994
                return $this->list = $this->array( $this->list );
3✔
2995
        }
2996

2997

2998
        /**
2999
         * Returns the keys of the all elements in a new map object.
3000
         *
3001
         * Examples:
3002
         *  Map::from( ['a', 'b'] );
3003
         *  Map::from( ['a' => 0, 'b' => 1] );
3004
         *
3005
         * Results:
3006
         * The first example returns a map containing [0, 1] while the second one will
3007
         * return a map with ['a', 'b'].
3008
         *
3009
         * @return self<int|string,mixed> New map
3010
         */
3011
        public function keys() : self
3012
        {
3013
                return new static( array_keys( $this->list() ) );
3✔
3014
        }
3015

3016

3017
        /**
3018
         * Sorts the elements by their keys in reverse order.
3019
         *
3020
         * Examples:
3021
         *  Map::from( ['b' => 0, 'a' => 1] )->krsort();
3022
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsort();
3023
         *
3024
         * Results:
3025
         *  ['a' => 1, 'b' => 0]
3026
         *  [0 => 'b', 1 => 'a']
3027
         *
3028
         * The parameter modifies how the keys are compared. Possible values are:
3029
         * - SORT_REGULAR : compare elements normally (don't change types)
3030
         * - SORT_NUMERIC : compare elements numerically
3031
         * - SORT_STRING : compare elements as strings
3032
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3033
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3034
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3035
         *
3036
         * The keys are preserved using this method and no new map is created.
3037
         *
3038
         * @param int $options Sort options for krsort()
3039
         * @return self<int|string,mixed> Updated map for fluid interface
3040
         */
3041
        public function krsort( int $options = SORT_REGULAR ) : self
3042
        {
3043
                krsort( $this->list(), $options );
9✔
3044
                return $this;
9✔
3045
        }
3046

3047

3048
        /**
3049
         * Sorts a copy of the elements by their keys in reverse order.
3050
         *
3051
         * Examples:
3052
         *  Map::from( ['b' => 0, 'a' => 1] )->krsorted();
3053
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsorted();
3054
         *
3055
         * Results:
3056
         *  ['a' => 1, 'b' => 0]
3057
         *  [0 => 'b', 1 => 'a']
3058
         *
3059
         * The parameter modifies how the keys are compared. Possible values are:
3060
         * - SORT_REGULAR : compare elements normally (don't change types)
3061
         * - SORT_NUMERIC : compare elements numerically
3062
         * - SORT_STRING : compare elements as strings
3063
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3064
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3065
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3066
         *
3067
         * The keys are preserved using this method and a new map is created.
3068
         *
3069
         * @param int $options Sort options for krsort()
3070
         * @return self<int|string,mixed> New map with sorted elements
3071
         */
3072
        public function krsorted( int $options = SORT_REGULAR ) : self
3073
        {
3074
                return ( clone $this )->krsort( $options );
3✔
3075
        }
3076

3077

3078
        /**
3079
         * Sorts the elements by their keys.
3080
         *
3081
         * Examples:
3082
         *  Map::from( ['b' => 0, 'a' => 1] )->ksort();
3083
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksort();
3084
         *
3085
         * Results:
3086
         *  ['a' => 1, 'b' => 0]
3087
         *  [0 => 'b', 1 => 'a']
3088
         *
3089
         * The parameter modifies how the keys are compared. Possible values are:
3090
         * - SORT_REGULAR : compare elements normally (don't change types)
3091
         * - SORT_NUMERIC : compare elements numerically
3092
         * - SORT_STRING : compare elements as strings
3093
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3094
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3095
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3096
         *
3097
         * The keys are preserved using this method and no new map is created.
3098
         *
3099
         * @param int $options Sort options for ksort()
3100
         * @return self<int|string,mixed> Updated map for fluid interface
3101
         */
3102
        public function ksort( int $options = SORT_REGULAR ) : self
3103
        {
3104
                ksort( $this->list(), $options );
9✔
3105
                return $this;
9✔
3106
        }
3107

3108

3109
        /**
3110
         * Sorts a copy of the elements by their keys.
3111
         *
3112
         * Examples:
3113
         *  Map::from( ['b' => 0, 'a' => 1] )->ksorted();
3114
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksorted();
3115
         *
3116
         * Results:
3117
         *  ['a' => 1, 'b' => 0]
3118
         *  [0 => 'b', 1 => 'a']
3119
         *
3120
         * The parameter modifies how the keys are compared. Possible values are:
3121
         * - SORT_REGULAR : compare elements normally (don't change types)
3122
         * - SORT_NUMERIC : compare elements numerically
3123
         * - SORT_STRING : compare elements as strings
3124
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3125
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3126
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3127
         *
3128
         * The keys are preserved using this method and a new map is created.
3129
         *
3130
         * @param int $options Sort options for ksort()
3131
         * @return self<int|string,mixed> New map with sorted elements
3132
         */
3133
        public function ksorted( int $options = SORT_REGULAR ) : self
3134
        {
3135
                return ( clone $this )->ksort( $options );
3✔
3136
        }
3137

3138

3139
        /**
3140
         * Returns the last element from the map.
3141
         *
3142
         * Examples:
3143
         *  Map::from( ['a', 'b'] )->last();
3144
         *  Map::from( [] )->last( 'x' );
3145
         *  Map::from( [] )->last( new \Exception( 'error' ) );
3146
         *  Map::from( [] )->last( function() { return rand(); } );
3147
         *
3148
         * Results:
3149
         * The first example will return 'b' and the second one 'x'. The third example
3150
         * will throw the exception passed if the map contains no elements. In the
3151
         * fourth example, a random value generated by the closure function will be
3152
         * returned.
3153
         *
3154
         * Using this method doesn't affect the internal array pointer.
3155
         *
3156
         * @param mixed $default Default value or exception if the map contains no elements
3157
         * @return mixed Last value of map, (generated) default value or an exception
3158
         */
3159
        public function last( $default = null )
3160
        {
3161
                if( !empty( $this->list() ) ) {
18✔
3162
                        return current( array_slice( $this->list(), -1, 1 ) );
6✔
3163
                }
3164

3165
                if( $default instanceof \Closure ) {
12✔
3166
                        return $default();
3✔
3167
                }
3168

3169
                if( $default instanceof \Throwable ) {
9✔
3170
                        throw $default;
3✔
3171
                }
3172

3173
                return $default;
6✔
3174
        }
3175

3176

3177
        /**
3178
         * Returns the last key from the map.
3179
         *
3180
         * Examples:
3181
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
3182
         *  Map::from( [] )->lastKey( 'x' );
3183
         *  Map::from( [] )->lastKey( new \Exception( 'error' ) );
3184
         *  Map::from( [] )->lastKey( function() { return rand(); } );
3185
         *
3186
         * Results:
3187
         * The first example will return 'a' and the second one 'x', the third one will throw
3188
         * an exception and the last one will return a random value.
3189
         *
3190
         * Using this method doesn't affect the internal array pointer.
3191
         *
3192
         * @param mixed $default Default value, closure or exception if the map contains no elements
3193
         * @return mixed Last key of map, (generated) default value or an exception
3194
         */
3195
        public function lastKey( $default = null )
3196
        {
3197
                $list = $this->list();
15✔
3198

3199
                // PHP 7.x compatibility
3200
                if( function_exists( 'array_key_last' ) ) {
15✔
3201
                        $key = array_key_last( $list );
15✔
3202
                } else {
3203
                        $key = key( array_slice( $list, -1, 1, true ) );
×
3204
                }
3205

3206
                if( $key !== null ) {
15✔
3207
                        return $key;
3✔
3208
                }
3209

3210
                if( $default instanceof \Closure ) {
12✔
3211
                        return $default();
3✔
3212
                }
3213

3214
                if( $default instanceof \Throwable ) {
9✔
3215
                        throw $default;
3✔
3216
                }
3217

3218
                return $default;
6✔
3219
        }
3220

3221

3222
        /**
3223
         * Removes the passed characters from the left of all strings.
3224
         *
3225
         * Examples:
3226
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
3227
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
3228
         *
3229
         * Results:
3230
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
3231
         *
3232
         * @param string $chars List of characters to trim
3233
         * @return self<int|string,mixed> Updated map for fluid interface
3234
         */
3235
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
3236
        {
3237
                foreach( $this->list() as &$entry )
3✔
3238
                {
3239
                        if( is_string( $entry ) ) {
3✔
3240
                                $entry = ltrim( $entry, $chars );
3✔
3241
                        }
3242
                }
3243

3244
                return $this;
3✔
3245
        }
3246

3247

3248
        /**
3249
         * Maps new values to the existing keys using the passed function and returns a new map for the result.
3250
         *
3251
         * Examples:
3252
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
3253
         *      return $value * 2;
3254
         *  } );
3255
         *
3256
         * Results:
3257
         *  ['a' => 4, 'b' => 8]
3258
         *
3259
         * The keys are preserved using this method.
3260
         *
3261
         * @param callable $callback Function with (value, key) parameters and returns computed result
3262
         * @return self<int|string,mixed> New map with the original keys and the computed values
3263
         * @see rekey() - Changes the keys according to the passed function
3264
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3265
         */
3266
        public function map( callable $callback ) : self
3267
        {
3268
                $list = $this->list();
3✔
3269
                $keys = array_keys( $list );
3✔
3270
                $map = array_map( $callback, array_values( $list ), $keys );
3✔
3271

3272
                return new static( array_combine( $keys, $map ) );
3✔
3273
        }
3274

3275

3276
        /**
3277
         * Returns the maximum value of all elements.
3278
         *
3279
         * Examples:
3280
         *  Map::from( [1, 3, 2, 5, 4] )->max()
3281
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
3282
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
3283
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
3284
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( fn( $val, $key ) => $val['i']['p'] ?? null )
3285
         *  Map::from( [50, 10, 30] )->max( fn( $val, $key ) => $key > 0 ? $val : null )
3286
         *
3287
         * Results:
3288
         * The first line will return "5", the second one "foo" and the third to fifth
3289
         * one return 50 while the last one will return 30.
3290
         *
3291
         * NULL values are removed before the comparison. If there are no values or all
3292
         * values are NULL, NULL is returned.
3293
         *
3294
         * This does also work for multi-dimensional arrays by passing the keys
3295
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3296
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3297
         * public properties of objects or objects implementing __isset() and __get() methods.
3298
         *
3299
         * Be careful comparing elements of different types because this can have
3300
         * unpredictable results due to the PHP comparison rules:
3301
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3302
         *
3303
         * @param Closure|string|null $col Closure, key or path to the value of the nested array or object
3304
         * @return mixed Maximum value or NULL if there are no elements in the map
3305
         */
3306
        public function max( $col = null )
3307
        {
3308
                $list = $this->list();
12✔
3309
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, fn( $v ) => $v !== null );
12✔
3310

3311
                return !empty( $vals ) ? max( $vals ) : null;
12✔
3312
        }
3313

3314

3315
        /**
3316
         * Merges the map with the given elements without returning a new map.
3317
         *
3318
         * Elements with the same non-numeric keys will be overwritten, elements
3319
         * with the same numeric keys will be added.
3320
         *
3321
         * Examples:
3322
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
3323
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
3324
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
3325
         *
3326
         * Results:
3327
         *  ['a', 'b', 'b', 'c']
3328
         *  ['a' => 1, 'b' => 4, 'c' => 6]
3329
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
3330
         *
3331
         * The method is similar to replace() but doesn't replace elements with
3332
         * the same numeric keys. If you want to be sure that all passed elements
3333
         * are added without replacing existing ones, use concat() instead.
3334
         *
3335
         * The keys are preserved using this method.
3336
         *
3337
         * @param iterable<int|string,mixed> $elements List of elements
3338
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
3339
         * @return self<int|string,mixed> Updated map for fluid interface
3340
         */
3341
        public function merge( iterable $elements, bool $recursive = false ) : self
3342
        {
3343
                if( $recursive ) {
9✔
3344
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
3✔
3345
                } else {
3346
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
6✔
3347
                }
3348

3349
                return $this;
9✔
3350
        }
3351

3352

3353
        /**
3354
         * Returns the minimum value of all elements.
3355
         *
3356
         * Examples:
3357
         *  Map::from( [2, 3, 1, 5, 4] )->min()
3358
         *  Map::from( ['baz', 'foo', 'bar'] )->min()
3359
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->min( 'p' )
3360
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( 'i/p' )
3361
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( fn( $val, $key ) => $val['i']['p'] ?? null )
3362
         *  Map::from( [10, 50, 30] )->min( fn( $val, $key ) => $key > 0 ? $val : null )
3363
         *
3364
         * Results:
3365
         * The first line will return "1", the second one "bar", the third one
3366
         * 10, and the forth to the last one 30.
3367
         *
3368
         * NULL values are removed before the comparison. If there are no values or all
3369
         * values are NULL, NULL is returned.
3370
         *
3371
         * This does also work for multi-dimensional arrays by passing the keys
3372
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3373
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3374
         * public properties of objects or objects implementing __isset() and __get() methods.
3375
         *
3376
         * Be careful comparing elements of different types because this can have
3377
         * unpredictable results due to the PHP comparison rules:
3378
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3379
         *
3380
         * @param Closure|string|null $key Closure, key or path to the value of the nested array or object
3381
         * @return mixed Minimum value or NULL if there are no elements in the map
3382
         */
3383
        public function min( $col = null )
3384
        {
3385
                $list = $this->list();
12✔
3386
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, fn( $v ) => $v !== null );
12✔
3387

3388
                return !empty( $vals ) ? min( $vals ) : null;
12✔
3389
        }
3390

3391

3392
        /**
3393
         * Tests if none of the elements are part of the map.
3394
         *
3395
         * Examples:
3396
         *  Map::from( ['a', 'b'] )->none( 'x' );
3397
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
3398
         *  Map::from( ['1', '2'] )->none( 2, true );
3399
         *  Map::from( ['a', 'b'] )->none( 'a' );
3400
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
3401
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
3402
         *
3403
         * Results:
3404
         * The first three examples will return TRUE while the other ones will return FALSE
3405
         *
3406
         * @param mixed|array $element Element or elements to search for in the map
3407
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3408
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
3409
         */
3410
        public function none( $element, bool $strict = false ) : bool
3411
        {
3412
                $list = $this->list();
3✔
3413

3414
                if( !is_array( $element ) ) {
3✔
3415
                        return !in_array( $element, $list, $strict );
3✔
3416
                };
3417

3418
                foreach( $element as $entry )
3✔
3419
                {
3420
                        if( in_array( $entry, $list, $strict ) === true ) {
3✔
3421
                                return false;
3✔
3422
                        }
3423
                }
3424

3425
                return true;
3✔
3426
        }
3427

3428

3429
        /**
3430
         * Returns every nth element from the map.
3431
         *
3432
         * Examples:
3433
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
3434
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
3435
         *
3436
         * Results:
3437
         *  ['a', 'c', 'e']
3438
         *  ['b', 'd', 'f']
3439
         *
3440
         * @param int $step Step width
3441
         * @param int $offset Number of element to start from (0-based)
3442
         * @return self<int|string,mixed> New map
3443
         */
3444
        public function nth( int $step, int $offset = 0 ) : self
3445
        {
3446
                if( $step < 1 ) {
9✔
3447
                        throw new \InvalidArgumentException( 'Step width must be greater than zero' );
3✔
3448
                }
3449

3450
                if( $step === 1 ) {
6✔
3451
                        return clone $this;
3✔
3452
                }
3453

3454
                $result = [];
3✔
3455
                $list = $this->list();
3✔
3456

3457
                while( !empty( $pair = array_slice( $list, $offset, 1, true ) ) )
3✔
3458
                {
3459
                        $result += $pair;
3✔
3460
                        $offset += $step;
3✔
3461
                }
3462

3463
                return new static( $result );
3✔
3464
        }
3465

3466

3467
        /**
3468
         * Determines if an element exists at an offset.
3469
         *
3470
         * Examples:
3471
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3472
         *  isset( $map['b'] );
3473
         *  isset( $map['c'] );
3474
         *  isset( $map['d'] );
3475
         *
3476
         * Results:
3477
         *  The first isset() will return TRUE while the second and third one will return FALSE
3478
         *
3479
         * @param int|string $key Key to check for
3480
         * @return bool TRUE if key exists, FALSE if not
3481
         */
3482
        public function offsetExists( $key ) : bool
3483
        {
3484
                return isset( $this->list()[$key] );
21✔
3485
        }
3486

3487

3488
        /**
3489
         * Returns an element at a given offset.
3490
         *
3491
         * Examples:
3492
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3493
         *  $map['b'];
3494
         *
3495
         * Results:
3496
         *  $map['b'] will return 3
3497
         *
3498
         * @param int|string $key Key to return the element for
3499
         * @return mixed Value associated to the given key
3500
         */
3501
        #[\ReturnTypeWillChange]
3502
        public function offsetGet( $key )
3503
        {
3504
                return $this->list()[$key] ?? null;
15✔
3505
        }
3506

3507

3508
        /**
3509
         * Sets the element at a given offset.
3510
         *
3511
         * Examples:
3512
         *  $map = Map::from( ['a' => 1] );
3513
         *  $map['b'] = 2;
3514
         *  $map[0] = 4;
3515
         *
3516
         * Results:
3517
         *  ['a' => 1, 'b' => 2, 0 => 4]
3518
         *
3519
         * @param int|string|null $key Key to set the element for or NULL to append value
3520
         * @param mixed $value New value set for the key
3521
         */
3522
        public function offsetSet( $key, $value ) : void
3523
        {
3524
                if( $key !== null ) {
9✔
3525
                        $this->list()[$key] = $value;
6✔
3526
                } else {
3527
                        $this->list()[] = $value;
6✔
3528
                }
3529
        }
3530

3531

3532
        /**
3533
         * Unsets the element at a given offset.
3534
         *
3535
         * Examples:
3536
         *  $map = Map::from( ['a' => 1] );
3537
         *  unset( $map['a'] );
3538
         *
3539
         * Results:
3540
         *  The map will be empty
3541
         *
3542
         * @param int|string $key Key for unsetting the item
3543
         */
3544
        public function offsetUnset( $key ) : void
3545
        {
3546
                unset( $this->list()[$key] );
6✔
3547
        }
3548

3549

3550
        /**
3551
         * Returns a new map with only those elements specified by the given keys.
3552
         *
3553
         * Examples:
3554
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3555
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3556
         *
3557
         * Results:
3558
         *  ['a' => 1]
3559
         *  [0 => 'b', 1 => 'c']
3560
         *
3561
         * The keys are preserved using this method.
3562
         *
3563
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3564
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3565
         */
3566
        public function only( $keys ) : self
3567
        {
3568
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
3✔
3569
        }
3570

3571

3572
        /**
3573
         * Returns a new map with elements ordered by the passed keys.
3574
         *
3575
         * If there are less keys passed than available in the map, the remaining
3576
         * elements are removed. Otherwise, if keys are passed that are not in the
3577
         * map, they will be also available in the returned map but their value is
3578
         * NULL.
3579
         *
3580
         * Examples:
3581
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3582
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3583
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3584
         *
3585
         * Results:
3586
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3587
         *  [0 => 'b', 1 => 'c', 2 => null]
3588
         *  [0 => 'b', 1 => 'c']
3589
         *
3590
         * The keys are preserved using this method.
3591
         *
3592
         * @param iterable<mixed> $keys Keys of the elements in the required order
3593
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3594
         */
3595
        public function order( iterable $keys ) : self
3596
        {
3597
                $result = [];
3✔
3598
                $list = $this->list();
3✔
3599

3600
                foreach( $keys as $key ) {
3✔
3601
                        $result[$key] = $list[$key] ?? null;
3✔
3602
                }
3603

3604
                return new static( $result );
3✔
3605
        }
3606

3607

3608
        /**
3609
         * Fill up to the specified length with the given value
3610
         *
3611
         * In case the given number is smaller than the number of element that are
3612
         * already in the list, the map is unchanged. If the size is positive, the
3613
         * new elements are padded on the right, if it's negative then the elements
3614
         * are padded on the left.
3615
         *
3616
         * Examples:
3617
         *  Map::from( [1, 2, 3] )->pad( 5 );
3618
         *  Map::from( [1, 2, 3] )->pad( -5 );
3619
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3620
         *  Map::from( [1, 2, 3] )->pad( 2 );
3621
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3622
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3623
         *
3624
         * Results:
3625
         *  [1, 2, 3, null, null]
3626
         *  [null, null, 1, 2, 3]
3627
         *  [1, 2, 3, '0', '0']
3628
         *  [1, 2, 3]
3629
         *  [0 => 1, 1 => 2, 2 => null]
3630
         *  ['a' => 1, 'b' => 2, 0 => 3]
3631
         *
3632
         * Associative keys are preserved, numerical keys are replaced and numerical
3633
         * keys are used for the new elements.
3634
         *
3635
         * @param int $size Total number of elements that should be in the list
3636
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3637
         * @return self<int|string,mixed> New map
3638
         */
3639
        public function pad( int $size, $value = null ) : self
3640
        {
3641
                return new static( array_pad( $this->list(), $size, $value ) );
3✔
3642
        }
3643

3644

3645
        /**
3646
         * Breaks the list of elements into the given number of groups.
3647
         *
3648
         * Examples:
3649
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3650
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3651
         *                return $idx % 3;
3652
         *        } );
3653
         *
3654
         * Results:
3655
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3656
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3657
         *
3658
         * The keys of the original map are preserved in the returned map.
3659
         *
3660
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3661
         * @return self<int|string,mixed> New map
3662
         */
3663
        public function partition( $number ) : self
3664
        {
3665
                $list = $this->list();
12✔
3666

3667
                if( empty( $list ) ) {
12✔
3668
                        return new static();
3✔
3669
                }
3670

3671
                $result = [];
9✔
3672

3673
                if( $number instanceof \Closure )
9✔
3674
                {
3675
                        foreach( $list as $idx => $item ) {
3✔
3676
                                $result[$number( $item, $idx )][$idx] = $item;
3✔
3677
                        }
3678

3679
                        return new static( $result );
3✔
3680
                }
3681

3682
                if( is_int( $number ) )
6✔
3683
                {
3684
                        $start = 0;
3✔
3685
                        $size = (int) ceil( count( $list ) / $number );
3✔
3686

3687
                        for( $i = 0; $i < $number; $i++ )
3✔
3688
                        {
3689
                                $result[] = array_slice( $list, $start, $size, true );
3✔
3690
                                $start += $size;
3✔
3691
                        }
3692

3693
                        return new static( $result );
3✔
3694
                }
3695

3696
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
3✔
3697
        }
3698

3699

3700
        /**
3701
         * Returns the percentage of all elements passing the test in the map.
3702
         *
3703
         * Examples:
3704
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50 );
3705
         *  Map::from( [] )->percentage( fn( $val, $key ) => true );
3706
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 100 );
3707
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 3 );
3708
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 0 );
3709
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50, -1 );
3710
         *
3711
         * Results:
3712
         * The first line will return "66.67", the second and third one "0.0", the forth
3713
         * one "33.333", the fifth one "33.0" and the last one "70.0" (66 rounded up).
3714
         *
3715
         * @param Closure $fcn Closure to filter the values in the nested array or object to compute the percentage
3716
         * @param int $precision Number of decimal digits use by the result value
3717
         * @return float Percentage of all elements passing the test in the map
3718
         */
3719
        public function percentage( \Closure $fcn, int $precision = 2 ) : float
3720
        {
3721
                $vals = array_filter( $this->list(), $fcn, ARRAY_FILTER_USE_BOTH );
3✔
3722

3723
                $cnt = count( $this->list() );
3✔
3724
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
3✔
3725
        }
3726

3727

3728
        /**
3729
         * Passes the map to the given callback and return the result.
3730
         *
3731
         * Examples:
3732
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3733
         *      return join( '-', $map->toArray() );
3734
         *  } );
3735
         *
3736
         * Results:
3737
         *  "a-b" will be returned
3738
         *
3739
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3740
         * @return mixed Result returned by the callback
3741
         */
3742
        public function pipe( \Closure $callback )
3743
        {
3744
                return $callback( $this );
3✔
3745
        }
3746

3747

3748
        /**
3749
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3750
         *
3751
         * This method is an alias for col(). For performance reasons, col() should
3752
         * be preferred because it uses one method call less than pluck().
3753
         *
3754
         * @param string|null $valuecol Name or path of the value property
3755
         * @param string|null $indexcol Name or path of the index property
3756
         * @return self<int|string,mixed> New map with mapped entries
3757
         * @see col() - Underlying method with same parameters and return value but better performance
3758
         */
3759
        public function pluck( ?string $valuecol = null, ?string $indexcol = null ) : self
3760
        {
3761
                return $this->col( $valuecol, $indexcol );
3✔
3762
        }
3763

3764

3765
        /**
3766
         * Returns and removes the last element from the map.
3767
         *
3768
         * Examples:
3769
         *  Map::from( ['a', 'b'] )->pop();
3770
         *
3771
         * Results:
3772
         *  "b" will be returned and the map only contains ['a'] afterwards
3773
         *
3774
         * @return mixed Last element of the map or null if empty
3775
         */
3776
        public function pop()
3777
        {
3778
                return array_pop( $this->list() );
6✔
3779
        }
3780

3781

3782
        /**
3783
         * Returns the numerical index of the value.
3784
         *
3785
         * Examples:
3786
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3787
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3788
         *      return $item === 'b';
3789
         *  } );
3790
         *
3791
         * Results:
3792
         * Both examples will return "1" because the value "b" is at the second position
3793
         * and the returned index is zero based so the first item has the index "0".
3794
         *
3795
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3796
         * @return int|null Position of the found value (zero based) or NULL if not found
3797
         */
3798
        public function pos( $value ) : ?int
3799
        {
3800
                $pos = 0;
45✔
3801
                $list = $this->list();
45✔
3802

3803
                if( $value instanceof \Closure )
45✔
3804
                {
3805
                        foreach( $list as $key => $item )
9✔
3806
                        {
3807
                                if( $value( $item, $key ) ) {
9✔
3808
                                        return $pos;
9✔
3809
                                }
3810

3811
                                ++$pos;
9✔
3812
                        }
3813

3814
                        return null;
×
3815
                }
3816

3817
                foreach( $list as $key => $item )
36✔
3818
                {
3819
                        if( $item === $value ) {
33✔
3820
                                return $pos;
30✔
3821
                        }
3822

3823
                        ++$pos;
18✔
3824
                }
3825

3826
                return null;
6✔
3827
        }
3828

3829

3830
        /**
3831
         * Adds a prefix in front of each map entry.
3832
         *
3833
         * By default, nested arrays are walked recursively so all entries at all levels are prefixed.
3834
         *
3835
         * Examples:
3836
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3837
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3838
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3839
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3840
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3841
         *  } );
3842
         *
3843
         * Results:
3844
         *  The first example returns ['1-a', '1-b'] while the second one will return
3845
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3846
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3847
         *  the closure will return ['145-a', '147-b'].
3848
         *
3849
         * The keys of the original map are preserved in the returned map.
3850
         *
3851
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3852
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3853
         * @return self<int|string,mixed> Updated map for fluid interface
3854
         */
3855
        public function prefix( $prefix, ?int $depth = null ) : self
3856
        {
3857
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
3✔
3858

3859
                        foreach( $list as $key => $item )
3✔
3860
                        {
3861
                                if( is_array( $item ) ) {
3✔
3862
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
3✔
3863
                                } else {
3864
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
3✔
3865
                                }
3866
                        }
3867

3868
                        return $list;
3✔
3869
                };
3✔
3870

3871
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
3✔
3872
                return $this;
3✔
3873
        }
3874

3875

3876
        /**
3877
         * Pushes an element onto the beginning of the map without returning a new map.
3878
         *
3879
         * This method is an alias for unshift().
3880
         *
3881
         * @param mixed $value Item to add at the beginning
3882
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3883
         * @return self<int|string,mixed> Updated map for fluid interface
3884
         * @see unshift() - Underlying method with same parameters and return value but better performance
3885
         */
3886
        public function prepend( $value, $key = null ) : self
3887
        {
3888
                return $this->unshift( $value, $key );
3✔
3889
        }
3890

3891

3892
        /**
3893
         * Returns and removes an element from the map by its key.
3894
         *
3895
         * Examples:
3896
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3897
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3898
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3899
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3900
         *
3901
         * Results:
3902
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3903
         * The second one will return "none" and the map content stays untouched. If you
3904
         * pass an exception as default value, it will throw that exception if the map
3905
         * contains no elements. In the fourth example, a random value generated by the
3906
         * closure function will be returned.
3907
         *
3908
         * @param int|string $key Key to retrieve the value for
3909
         * @param mixed $default Default value if key isn't available
3910
         * @return mixed Value from map or default value
3911
         */
3912
        public function pull( $key, $default = null )
3913
        {
3914
                $value = $this->get( $key, $default );
12✔
3915
                unset( $this->list()[$key] );
9✔
3916

3917
                return $value;
9✔
3918
        }
3919

3920

3921
        /**
3922
         * Pushes an element onto the end of the map without returning a new map.
3923
         *
3924
         * Examples:
3925
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3926
         *
3927
         * Results:
3928
         *  ['a', 'b', 'aa']
3929
         *
3930
         * @param mixed $value Value to add to the end
3931
         * @return self<int|string,mixed> Updated map for fluid interface
3932
         */
3933
        public function push( $value ) : self
3934
        {
3935
                $this->list()[] = $value;
9✔
3936
                return $this;
9✔
3937
        }
3938

3939

3940
        /**
3941
         * Sets the given key and value in the map without returning a new map.
3942
         *
3943
         * This method is an alias for set(). For performance reasons, set() should be
3944
         * preferred because it uses one method call less than put().
3945
         *
3946
         * @param int|string $key Key to set the new value for
3947
         * @param mixed $value New element that should be set
3948
         * @return self<int|string,mixed> Updated map for fluid interface
3949
         * @see set() - Underlying method with same parameters and return value but better performance
3950
         */
3951
        public function put( $key, $value ) : self
3952
        {
3953
                return $this->set( $key, $value );
3✔
3954
        }
3955

3956

3957
        /**
3958
         * Returns one or more random element from the map incl. their keys.
3959
         *
3960
         * Examples:
3961
         *  Map::from( [2, 4, 8, 16] )->random();
3962
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3963
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3964
         *
3965
         * Results:
3966
         * The first example will return a map including [0 => 8] or any other value,
3967
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3968
         * and the third example will return a map of the whole list in random order. The
3969
         * less elements are in the map, the less random the order will be, especially if
3970
         * the maximum number of values is high or close to the number of elements.
3971
         *
3972
         * The keys of the original map are preserved in the returned map.
3973
         *
3974
         * @param int $max Maximum number of elements that should be returned
3975
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
3976
         * @throws \InvalidArgumentException If requested number of elements is less than 1
3977
         */
3978
        public function random( int $max = 1 ) : self
3979
        {
3980
                if( $max < 1 ) {
15✔
3981
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
3✔
3982
                }
3983

3984
                $list = $this->list();
12✔
3985

3986
                if( empty( $list ) ) {
12✔
3987
                        return new static();
3✔
3988
                }
3989

3990
                if( ( $num = count( $list ) ) < $max ) {
9✔
3991
                        $max = $num;
3✔
3992
                }
3993

3994
                $keys = array_rand( $list, $max );
9✔
3995

3996
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
9✔
3997
        }
3998

3999

4000
        /**
4001
         * Iteratively reduces the array to a single value using a callback function.
4002
         * Afterwards, the map will be empty.
4003
         *
4004
         * Examples:
4005
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
4006
         *      return $result += $value;
4007
         *  }, 10 );
4008
         *
4009
         * Results:
4010
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
4011
         *
4012
         * @param callable $callback Function with (result, value) parameters and returns result
4013
         * @param mixed $initial Initial value when computing the result
4014
         * @return mixed Value computed by the callback function
4015
         */
4016
        public function reduce( callable $callback, $initial = null )
4017
        {
4018
                return array_reduce( $this->list(), $callback, $initial );
3✔
4019
        }
4020

4021

4022
        /**
4023
         * Removes all matched elements and returns a new map.
4024
         *
4025
         * Examples:
4026
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
4027
         *      return $value < 'm';
4028
         *  } );
4029
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
4030
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
4031
         *
4032
         * Results:
4033
         *  [13 => 'm', 30 => 'z']
4034
         *  [2 => 'a', 30 => 'z']
4035
         *  [6 => null]
4036
         *
4037
         * This method is the inverse of the filter() and should return TRUE if the
4038
         * item should be removed from the returned map.
4039
         *
4040
         * If no callback is passed, all values which are NOT empty, null or false will be
4041
         * removed. The keys of the original map are preserved in the returned map.
4042
         *
4043
         * @param Closure|mixed $callback Function with (item, key) parameter which returns TRUE/FALSE
4044
         * @return self<int|string,mixed> New map
4045
         */
4046
        public function reject( $callback = true ) : self
4047
        {
4048
                $result = [];
9✔
4049

4050
                if( $callback instanceof \Closure )
9✔
4051
                {
4052
                        foreach( $this->list() as $key => $value )
3✔
4053
                        {
4054
                                if( !$callback( $value, $key ) ) {
3✔
4055
                                        $result[$key] = $value;
3✔
4056
                                }
4057
                        }
4058
                }
4059
                else
4060
                {
4061
                        foreach( $this->list() as $key => $value )
6✔
4062
                        {
4063
                                if( $value != $callback ) {
6✔
4064
                                        $result[$key] = $value;
6✔
4065
                                }
4066
                        }
4067
                }
4068

4069
                return new static( $result );
9✔
4070
        }
4071

4072

4073
        /**
4074
         * Changes the keys according to the passed function.
4075
         *
4076
         * Examples:
4077
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
4078
         *      return 'key-' . $key;
4079
         *  } );
4080
         *
4081
         * Results:
4082
         *  ['key-a' => 2, 'key-b' => 4]
4083
         *
4084
         * @param callable $callback Function with (value, key) parameters and returns new key
4085
         * @return self<int|string,mixed> New map with new keys and original values
4086
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
4087
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
4088
         */
4089
        public function rekey( callable $callback ) : self
4090
        {
4091
                $list = $this->list();
3✔
4092
                $newKeys = array_map( $callback, $list, array_keys( $list ) );
3✔
4093

4094
                return new static( array_combine( $newKeys, array_values( $list ) ) );
3✔
4095
        }
4096

4097

4098
        /**
4099
         * Removes one or more elements from the map by its keys without returning a new map.
4100
         *
4101
         * Examples:
4102
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
4103
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
4104
         *
4105
         * Results:
4106
         * The first example will result in [2 => 'b'] while the second one resulting
4107
         * in an empty list
4108
         *
4109
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
4110
         * @return self<int|string,mixed> Updated map for fluid interface
4111
         */
4112
        public function remove( $keys ) : self
4113
        {
4114
                foreach( $this->array( $keys ) as $key ) {
15✔
4115
                        unset( $this->list()[$key] );
15✔
4116
                }
4117

4118
                return $this;
15✔
4119
        }
4120

4121

4122
        /**
4123
         * Replaces elements in the map with the given elements without returning a new map.
4124
         *
4125
         * Examples:
4126
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
4127
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
4128
         *
4129
         * Results:
4130
         *  ['a' => 2, 2 => 'b']
4131
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
4132
         *
4133
         * The method is similar to merge() but it also replaces elements with numeric
4134
         * keys. These would be added by merge() with a new numeric key.
4135
         *
4136
         * The keys are preserved using this method.
4137
         *
4138
         * @param iterable<int|string,mixed> $elements List of elements
4139
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
4140
         * @return self<int|string,mixed> Updated map for fluid interface
4141
         */
4142
        public function replace( iterable $elements, bool $recursive = true ) : self
4143
        {
4144
                if( $recursive ) {
15✔
4145
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
12✔
4146
                } else {
4147
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
3✔
4148
                }
4149

4150
                return $this;
15✔
4151
        }
4152

4153

4154
        /**
4155
         * Returns only the items matching the value (and key) from the map.
4156
         *
4157
         * Examples:
4158
         *  Map::from( ['a', 'b', 'a'] )->restrict( 'a' );
4159
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->restrict( 'test', 'name' );
4160
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->restrict( fn( $v, $k ) => $v['name'] === 'user' );
4161
         *  Map::from( ['a', 'b', 'a'] )->restrict( fn( $v, $k ) => $v === 'a' && $k < 2 );
4162
         *
4163
         * Results:
4164
         *  [0 => 'a', 2 => 'a']
4165
         *  [0 => ['name' => 'test'], 2 => ['name' => 'test']]
4166
         *  [1 => ['name' => 'user']]
4167
         *  [0 => 'a']
4168
         *
4169
         * The keys are preserved in the returned map.
4170
         *
4171
         * @param \Closure|mixed $value Closure with (item, key) parameter or element to test against
4172
         * @param string|int|null $key Key to compare the value to if $value is not a closure
4173
         * @return self<int|string,mixed> New map with matching items only
4174
         */
4175
        public function restrict( $value = null, $key = null ) : self
4176
        {
4177
                $filter = $value;
30✔
4178

4179
                if( !( $value instanceof \Closure ) )
30✔
4180
                {
4181
                        if( $key === null )
24✔
4182
                        {
4183
                                $filter = function( $v ) use ( $value ) {
21✔
4184
                                        return $v === $value;
21✔
4185
                                };
21✔
4186
                        }
4187
                        else
4188
                        {
4189
                                $filter = function( $v, $k ) use ( $key, $value ) {
3✔
4190
                                        return ( $v[$key] ?? null ) === $value;
3✔
4191
                                };
3✔
4192
                        }
4193
                }
4194

4195
                return $this->filter( $filter );
30✔
4196
        }
4197

4198

4199
        /**
4200
         * Reverses the element order with keys without returning a new map.
4201
         *
4202
         * Examples:
4203
         *  Map::from( ['a', 'b'] )->reverse();
4204
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
4205
         *
4206
         * Results:
4207
         *  ['b', 'a']
4208
         *  ['last' => 'user', 'name' => 'test']
4209
         *
4210
         * The keys are preserved using this method.
4211
         *
4212
         * @return self<int|string,mixed> Updated map for fluid interface
4213
         * @see reversed() - Reverses the element order in a copy of the map
4214
         */
4215
        public function reverse() : self
4216
        {
4217
                $this->list = array_reverse( $this->list(), true );
12✔
4218
                return $this;
12✔
4219
        }
4220

4221

4222
        /**
4223
         * Reverses the element order in a copy of the map.
4224
         *
4225
         * Examples:
4226
         *  Map::from( ['a', 'b'] )->reversed();
4227
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reversed();
4228
         *
4229
         * Results:
4230
         *  ['b', 'a']
4231
         *  ['last' => 'user', 'name' => 'test']
4232
         *
4233
         * The keys are preserved using this method and a new map is created before reversing the elements.
4234
         * Thus, reverse() should be preferred for performance reasons if possible.
4235
         *
4236
         * @return self<int|string,mixed> New map with a reversed copy of the elements
4237
         * @see reverse() - Reverses the element order with keys without returning a new map
4238
         */
4239
        public function reversed() : self
4240
        {
4241
                return ( clone $this )->reverse();
6✔
4242
        }
4243

4244

4245
        /**
4246
         * Sorts all elements in reverse order using new keys.
4247
         *
4248
         * Examples:
4249
         *  Map::from( ['a' => 1, 'b' => 0] )->rsort();
4250
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsort();
4251
         *
4252
         * Results:
4253
         *  [0 => 1, 1 => 0]
4254
         *  [0 => 'b', 1 => 'a']
4255
         *
4256
         * The parameter modifies how the values are compared. Possible parameter values are:
4257
         * - SORT_REGULAR : compare elements normally (don't change types)
4258
         * - SORT_NUMERIC : compare elements numerically
4259
         * - SORT_STRING : compare elements as strings
4260
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4261
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4262
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4263
         *
4264
         * The keys aren't preserved and elements get a new index. No new map is created
4265
         *
4266
         * @param int $options Sort options for rsort()
4267
         * @return self<int|string,mixed> Updated map for fluid interface
4268
         */
4269
        public function rsort( int $options = SORT_REGULAR ) : self
4270
        {
4271
                rsort( $this->list(), $options );
9✔
4272
                return $this;
9✔
4273
        }
4274

4275

4276
        /**
4277
         * Sorts a copy of all elements in reverse order using new keys.
4278
         *
4279
         * Examples:
4280
         *  Map::from( ['a' => 1, 'b' => 0] )->rsorted();
4281
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsorted();
4282
         *
4283
         * Results:
4284
         *  [0 => 1, 1 => 0]
4285
         *  [0 => 'b', 1 => 'a']
4286
         *
4287
         * The parameter modifies how the values are compared. Possible parameter values are:
4288
         * - SORT_REGULAR : compare elements normally (don't change types)
4289
         * - SORT_NUMERIC : compare elements numerically
4290
         * - SORT_STRING : compare elements as strings
4291
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4292
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4293
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4294
         *
4295
         * The keys aren't preserved, elements get a new index and a new map is created.
4296
         *
4297
         * @param int $options Sort options for rsort()
4298
         * @return self<int|string,mixed> New map with sorted elements
4299
         */
4300
        public function rsorted( int $options = SORT_REGULAR ) : self
4301
        {
4302
                return ( clone $this )->rsort( $options );
3✔
4303
        }
4304

4305

4306
        /**
4307
         * Removes the passed characters from the right of all strings.
4308
         *
4309
         * Examples:
4310
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
4311
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
4312
         *
4313
         * Results:
4314
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
4315
         *
4316
         * @param string $chars List of characters to trim
4317
         * @return self<int|string,mixed> Updated map for fluid interface
4318
         */
4319
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
4320
        {
4321
                foreach( $this->list() as &$entry )
3✔
4322
                {
4323
                        if( is_string( $entry ) ) {
3✔
4324
                                $entry = rtrim( $entry, $chars );
3✔
4325
                        }
4326
                }
4327

4328
                return $this;
3✔
4329
        }
4330

4331

4332
        /**
4333
         * Searches the map for a given value and return the corresponding key if successful.
4334
         *
4335
         * Examples:
4336
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
4337
         *  Map::from( [1, 2, 3] )->search( '2', true );
4338
         *
4339
         * Results:
4340
         * The first example will return 1 (array index) while the second one will
4341
         * return NULL because the types doesn't match (int vs. string)
4342
         *
4343
         * @param mixed $value Item to search for
4344
         * @param bool $strict TRUE if type of the element should be checked too
4345
         * @return int|string|null Key associated to the value or null if not found
4346
         */
4347
        public function search( $value, $strict = true )
4348
        {
4349
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
3✔
4350
                        return $result;
3✔
4351
                }
4352

4353
                return null;
3✔
4354
        }
4355

4356

4357
        /**
4358
         * Sets the separator for paths to values in multi-dimensional arrays or objects.
4359
         *
4360
         * This method only changes the separator for the current map instance. To
4361
         * change the separator for all maps created afterwards, use the static
4362
         * delimiter() method instead.
4363
         *
4364
         * Examples:
4365
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '.' )->get( 'foo.bar' );
4366
         *
4367
         * Results:
4368
         *  'baz'
4369
         *
4370
         * @param string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
4371
         * @return self<int|string,mixed> Same map for fluid interface
4372
         */
4373
        public function sep( string $char ) : self
4374
        {
4375
                $this->sep = $char;
9✔
4376
                return $this;
9✔
4377
        }
4378

4379

4380
        /**
4381
         * Sets an element in the map by key without returning a new map.
4382
         *
4383
         * Examples:
4384
         *  Map::from( ['a'] )->set( 1, 'b' );
4385
         *  Map::from( ['a'] )->set( 0, 'b' );
4386
         *
4387
         * Results:
4388
         *  ['a', 'b']
4389
         *  ['b']
4390
         *
4391
         * @param int|string $key Key to set the new value for
4392
         * @param mixed $value New element that should be set
4393
         * @return self<int|string,mixed> Updated map for fluid interface
4394
         */
4395
        public function set( $key, $value ) : self
4396
        {
4397
                $this->list()[(string) $key] = $value;
15✔
4398
                return $this;
15✔
4399
        }
4400

4401

4402
        /**
4403
         * Returns and removes the first element from the map.
4404
         *
4405
         * Examples:
4406
         *  Map::from( ['a', 'b'] )->shift();
4407
         *  Map::from( [] )->shift();
4408
         *
4409
         * Results:
4410
         * The first example returns "a" and shortens the map to ['b'] only while the
4411
         * second example will return NULL
4412
         *
4413
         * Performance note:
4414
         * The bigger the list, the higher the performance impact because shift()
4415
         * reindexes all existing elements. Usually, it's better to reverse() the list
4416
         * and pop() entries from the list afterwards if a significant number of elements
4417
         * should be removed from the list:
4418
         *
4419
         *  $map->reverse()->pop();
4420
         * instead of
4421
         *  $map->shift( 'a' );
4422
         *
4423
         * @return mixed|null Value from map or null if not found
4424
         */
4425
        public function shift()
4426
        {
4427
                return array_shift( $this->list() );
3✔
4428
        }
4429

4430

4431
        /**
4432
         * Shuffles the elements in the map without returning a new map.
4433
         *
4434
         * Examples:
4435
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle();
4436
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle( true );
4437
         *
4438
         * Results:
4439
         * The map in the first example will contain "a" and "b" in random order and
4440
         * with new keys assigned. The second call will also return all values in
4441
         * random order but preserves the keys of the original list.
4442
         *
4443
         * @param bool $assoc True to preserve keys, false to assign new keys
4444
         * @return self<int|string,mixed> Updated map for fluid interface
4445
         * @see shuffled() - Shuffles the elements in a copy of the map
4446
         */
4447
        public function shuffle( bool $assoc = false ) : self
4448
        {
4449
                if( $assoc )
9✔
4450
                {
4451
                        $list = $this->list();
3✔
4452
                        $keys = array_keys( $list );
3✔
4453
                        shuffle( $keys );
3✔
4454
                        $items = [];
3✔
4455

4456
                        foreach( $keys as $key ) {
3✔
4457
                                $items[$key] = $list[$key];
3✔
4458
                        }
4459

4460
                        $this->list = $items;
3✔
4461
                }
4462
                else
4463
                {
4464
                        shuffle( $this->list() );
6✔
4465
                }
4466

4467
                return $this;
9✔
4468
        }
4469

4470

4471
        /**
4472
         * Shuffles the elements in a copy of the map.
4473
         *
4474
         * Examples:
4475
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled();
4476
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled( true );
4477
         *
4478
         * Results:
4479
         * The map in the first example will contain "a" and "b" in random order and
4480
         * with new keys assigned. The second call will also return all values in
4481
         * random order but preserves the keys of the original list.
4482
         *
4483
         * @param bool $assoc True to preserve keys, false to assign new keys
4484
         * @return self<int|string,mixed> New map with a shuffled copy of the elements
4485
         * @see shuffle() - Shuffles the elements in the map without returning a new map
4486
         */
4487
        public function shuffled( bool $assoc = false ) : self
4488
        {
4489
                return ( clone $this )->shuffle( $assoc );
3✔
4490
        }
4491

4492

4493
        /**
4494
         * Returns a new map with the given number of items skipped.
4495
         *
4496
         * Examples:
4497
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
4498
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
4499
         *      return $item < 4;
4500
         *  } );
4501
         *
4502
         * Results:
4503
         *  [2 => 3, 3 => 4]
4504
         *  [3 => 4]
4505
         *
4506
         * The keys of the items returned in the new map are the same as in the original one.
4507
         *
4508
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4509
         * @return self<int|string,mixed> New map
4510
         */
4511
        public function skip( $offset ) : self
4512
        {
4513
                if( is_numeric( $offset ) ) {
9✔
4514
                        return new static( array_slice( $this->list(), (int) $offset, null, true ) );
3✔
4515
                }
4516

4517
                if( $offset instanceof \Closure ) {
6✔
4518
                        return new static( array_slice( $this->list(), $this->until( $this->list(), $offset ), null, true ) );
3✔
4519
                }
4520

4521
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as first argument for skip()' );
3✔
4522
        }
4523

4524

4525
        /**
4526
         * Returns a map with the slice from the original map.
4527
         *
4528
         * Examples:
4529
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
4530
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
4531
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
4532
         *
4533
         * Results:
4534
         * The first example will return ['b', 'c'] and the second one ['b'] only.
4535
         * The third example returns ['c'] because the slice starts at the second
4536
         * last value and ends before the last value.
4537
         *
4538
         * The rules for offsets are:
4539
         * - If offset is non-negative, the sequence will start at that offset
4540
         * - If offset is negative, the sequence will start that far from the end
4541
         *
4542
         * Similar for the length:
4543
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4544
         * - If the array is shorter than the length, then only the available array elements will be present
4545
         * - If length is given and is negative then the sequence will stop that many elements from the end
4546
         * - If it is omitted, then the sequence will have everything from offset up until the end
4547
         *
4548
         * The keys of the items returned in the new map are the same as in the original one.
4549
         *
4550
         * @param int $offset Number of elements to start from
4551
         * @param int|null $length Number of elements to return or NULL for no limit
4552
         * @return self<int|string,mixed> New map
4553
         */
4554
        public function slice( int $offset, ?int $length = null ) : self
4555
        {
4556
                return new static( array_slice( $this->list(), $offset, $length, true ) );
18✔
4557
        }
4558

4559

4560
        /**
4561
         * Returns a new map containing sliding windows of the original map.
4562
         *
4563
         * Examples:
4564
         *  Map::from( [1, 2, 3, 4] )->sliding( 2 );
4565
         *  Map::from( [1, 2, 3, 4] )->sliding( 3, 2 );
4566
         *
4567
         * Results:
4568
         * The first example will return [[0 => 1, 1 => 2], [1 => 2, 2 => 3], [2 => 3, 3 => 4]]
4569
         * while the second one will return [[0 => 1, 1 => 2, 2 => 3], [2 => 3, 3 => 4, 4 => 5]]
4570
         *
4571
         * @param int $size Size of each window
4572
         * @param int $step Step size to move the window
4573
         * @return self<int,array<int|string,mixed>> New map containing arrays for each window
4574
         */
4575
        public function sliding( int $size = 2, int $step = 1 ) : self
4576
        {
4577
                $result = [];
6✔
4578
                $chunks = floor( ( $this->count() - $size ) / $step ) + 1;
6✔
4579

4580
                for( $i = 0; $i < $chunks; $i++ ) {
6✔
4581
                        $result[] = array_slice( $this->list(), $i * $step, $size, true );
6✔
4582
                }
4583

4584
                return new static( $result );
6✔
4585
        }
4586

4587

4588
        /**
4589
         * Returns the matching item, but only if one matching item exists.
4590
         *
4591
         * Examples:
4592
         *  Map::from( ['a', 'b'] )->sole( 'a' );
4593
         *  Map::from( ['a', 'b', 'a'] )->restrict( fn( $v, $k ) => $v === 'a' && $k < 2 );
4594
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->restrict( fn( $v, $k ) => $v['name'] === 'user' );
4595
         *  Map::from( ['b', 'c'] )->sole( 'a' );
4596
         *  Map::from( ['a', 'b', 'a'] )->sole( 'a' );
4597
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->restrict( 'test', 'name' );
4598
         *
4599
         * Results:
4600
         * The first two examples will return "a" while the third one will return [1 => ['name' => 'user']].
4601
         * All other examples throw a LengthException because more than one item matches the test.
4602
         *
4603
         * @param \Closure|mixed $values Closure with (item, key) parameter or element to test against
4604
         * @param string|int|null $key Key to compare the value for if $values is not a closure
4605
         * @return mixed Value from map if exactly one matching item exists
4606
         * @throws \LengthException If no items or more than one item is found
4607
         */
4608
        public function sole( $value = null, $key = null )
4609
        {
4610
                $items = $this->restrict( $value, $key );
9✔
4611

4612
                if( $items->count() > 1 ) {
9✔
4613
                        throw new \LengthException( 'Multiple items found' );
3✔
4614
                }
4615

4616
                return $items->first( new \LengthException( 'No items found' ) );
6✔
4617
        }
4618

4619

4620
        /**
4621
         * Tests if at least one element passes the test or is part of the map.
4622
         *
4623
         * Examples:
4624
         *  Map::from( ['a', 'b'] )->some( 'a' );
4625
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
4626
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
4627
         *    return $item === 'a';
4628
         *  } );
4629
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
4630
         *  Map::from( ['1', '2'] )->some( [2], true );
4631
         *
4632
         * Results:
4633
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
4634
         *
4635
         * @param \Closure|iterable|mixed $values Closure with (item, key) parameter, element or list of elements to test against
4636
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
4637
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
4638
         */
4639
        public function some( $values, bool $strict = false ) : bool
4640
        {
4641
                $list = $this->list();
18✔
4642

4643
                if( is_iterable( $values ) )
18✔
4644
                {
4645
                        foreach( $values as $entry )
9✔
4646
                        {
4647
                                if( in_array( $entry, $list, $strict ) === true ) {
9✔
4648
                                        return true;
9✔
4649
                                }
4650
                        }
4651

4652
                        return false;
6✔
4653
                }
4654

4655
                if( $values instanceof \Closure )
12✔
4656
                {
4657
                        foreach( $list as $key => $item )
6✔
4658
                        {
4659
                                if( $values( $item, $key ) ) {
6✔
4660
                                        return true;
6✔
4661
                                }
4662
                        }
4663
                }
4664

4665
                return in_array( $values, $list, $strict );
12✔
4666
        }
4667

4668

4669
        /**
4670
         * Sorts all elements in-place using new keys.
4671
         *
4672
         * Examples:
4673
         *  Map::from( ['a' => 1, 'b' => 0] )->sort();
4674
         *  Map::from( [0 => 'b', 1 => 'a'] )->sort();
4675
         *
4676
         * Results:
4677
         *  [0 => 0, 1 => 1]
4678
         *  [0 => 'a', 1 => 'b']
4679
         *
4680
         * The parameter modifies how the values are compared. Possible parameter values are:
4681
         * - SORT_REGULAR : compare elements normally (don't change types)
4682
         * - SORT_NUMERIC : compare elements numerically
4683
         * - SORT_STRING : compare elements as strings
4684
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4685
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4686
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4687
         *
4688
         * The keys aren't preserved and elements get a new index. No new map is created.
4689
         *
4690
         * @param int $options Sort options for PHP sort()
4691
         * @return self<int|string,mixed> Updated map for fluid interface
4692
         * @see sorted() - Sorts elements in a copy of the map
4693
         */
4694
        public function sort( int $options = SORT_REGULAR ) : self
4695
        {
4696
                sort( $this->list(), $options );
15✔
4697
                return $this;
15✔
4698
        }
4699

4700

4701
        /**
4702
         * Sorts the elements in a copy of the map using new keys.
4703
         *
4704
         * Examples:
4705
         *  Map::from( ['a' => 1, 'b' => 0] )->sorted();
4706
         *  Map::from( [0 => 'b', 1 => 'a'] )->sorted();
4707
         *
4708
         * Results:
4709
         *  [0 => 0, 1 => 1]
4710
         *  [0 => 'a', 1 => 'b']
4711
         *
4712
         * The parameter modifies how the values are compared. Possible parameter values are:
4713
         * - SORT_REGULAR : compare elements normally (don't change types)
4714
         * - SORT_NUMERIC : compare elements numerically
4715
         * - SORT_STRING : compare elements as strings
4716
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4717
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4718
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4719
         *
4720
         * The keys aren't preserved and elements get a new index and a new map is created before sorting the elements.
4721
         * Thus, sort() should be preferred for performance reasons if possible. A new map is created by calling this method.
4722
         *
4723
         * @param int $options Sort options for PHP sort()
4724
         * @return self<int|string,mixed> New map with a sorted copy of the elements
4725
         * @see sort() - Sorts elements in-place in the original map
4726
         */
4727
        public function sorted( int $options = SORT_REGULAR ) : self
4728
        {
4729
                return ( clone $this )->sort( $options );
6✔
4730
        }
4731

4732

4733
        /**
4734
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4735
         *
4736
         * Examples:
4737
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4738
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4739
         *
4740
         * Results:
4741
         * The first example removes all entries after "a", so only ['a'] will be left
4742
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4743
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4744
         * ['a', 'x', 'y', 'c'] afterwards.
4745
         *
4746
         * The rules for offsets are:
4747
         * - If offset is non-negative, the sequence will start at that offset
4748
         * - If offset is negative, the sequence will start that far from the end
4749
         *
4750
         * Similar for the length:
4751
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4752
         * - If the array is shorter than the length, then only the available array elements will be present
4753
         * - If length is given and is negative then the sequence will stop that many elements from the end
4754
         * - If it is omitted, then the sequence will have everything from offset up until the end
4755
         *
4756
         * Numerical array indexes are NOT preserved.
4757
         *
4758
         * @param int $offset Number of elements to start from
4759
         * @param int|null $length Number of elements to remove, NULL for all
4760
         * @param mixed $replacement List of elements to insert
4761
         * @return self<int|string,mixed> New map
4762
         */
4763
        public function splice( int $offset, ?int $length = null, $replacement = [] ) : self
4764
        {
4765
                // PHP 7.x doesn't allow to pass NULL as replacement
4766
                if( $length === null ) {
15✔
4767
                        $length = count( $this->list() );
6✔
4768
                }
4769

4770
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
15✔
4771
        }
4772

4773

4774
        /**
4775
         * Returns the strings after the passed value.
4776
         *
4777
         * Examples:
4778
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4779
         *  Map::from( ['abc'] )->strAfter( '' );
4780
         *  Map::from( ['abc'] )->strAfter( 'b' );
4781
         *  Map::from( ['abc'] )->strAfter( 'c' );
4782
         *  Map::from( ['abc'] )->strAfter( 'x' );
4783
         *  Map::from( [''] )->strAfter( '' );
4784
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4785
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4786
         *
4787
         * Results:
4788
         *  ['üß']
4789
         *  ['abc']
4790
         *  ['c']
4791
         *  ['']
4792
         *  []
4793
         *  []
4794
         *  ['1', '1', '1']
4795
         *  ['0', '0']
4796
         *
4797
         * All scalar values (bool, int, float, string) will be converted to strings.
4798
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4799
         *
4800
         * @param string $value Character or string to search for
4801
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4802
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4803
         * @return self<int|string,mixed> New map
4804
         */
4805
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4806
        {
4807
                $list = [];
3✔
4808
                $len = mb_strlen( $value, $encoding );
3✔
4809
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
3✔
4810

4811
                foreach( $this->list() as $key => $entry )
3✔
4812
                {
4813
                        if( is_scalar( $entry ) )
3✔
4814
                        {
4815
                                $pos = null;
3✔
4816
                                $str = (string) $entry;
3✔
4817

4818
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
3✔
4819
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
3✔
4820
                                } elseif( $str !== '' && $pos !== false ) {
3✔
4821
                                        $list[$key] = $str;
3✔
4822
                                }
4823
                        }
4824
                }
4825

4826
                return new static( $list );
3✔
4827
        }
4828

4829

4830
        /**
4831
         * Returns the strings before the passed value.
4832
         *
4833
         * Examples:
4834
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4835
         *  Map::from( ['abc'] )->strBefore( '' );
4836
         *  Map::from( ['abc'] )->strBefore( 'b' );
4837
         *  Map::from( ['abc'] )->strBefore( 'a' );
4838
         *  Map::from( ['abc'] )->strBefore( 'x' );
4839
         *  Map::from( [''] )->strBefore( '' );
4840
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strBefore( '' );
4841
         *  Map::from( [0, 0.0, false, []] )->strBefore( '' );
4842
         *
4843
         * Results:
4844
         *  ['äö']
4845
         *  ['abc']
4846
         *  ['a']
4847
         *  ['']
4848
         *  []
4849
         *  []
4850
         *  ['1', '1', '1']
4851
         *  ['0', '0']
4852
         *
4853
         * All scalar values (bool, int, float, string) will be converted to strings.
4854
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4855
         *
4856
         * @param string $value Character or string to search for
4857
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4858
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4859
         * @return self<int|string,mixed> New map
4860
         */
4861
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4862
        {
4863
                $list = [];
3✔
4864
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
3✔
4865

4866
                foreach( $this->list() as $key => $entry )
3✔
4867
                {
4868
                        if( is_scalar( $entry ) )
3✔
4869
                        {
4870
                                $pos = null;
3✔
4871
                                $str = (string) $entry;
3✔
4872

4873
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
3✔
4874
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
3✔
4875
                                } elseif( $str !== '' && $pos !== false ) {
3✔
4876
                                        $list[$key] = $str;
3✔
4877
                                }
4878
                        }
4879
                }
4880

4881
                return new static( $list );
3✔
4882
        }
4883

4884

4885
        /**
4886
         * Compares the value against all map elements.
4887
         *
4888
         * Examples:
4889
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
4890
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
4891
         *  Map::from( [123, 12.3] )->compare( '12.3' );
4892
         *  Map::from( [false, true] )->compare( '1' );
4893
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
4894
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
4895
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
4896
         *
4897
         * Results:
4898
         * The first four examples return TRUE, the last three examples will return FALSE.
4899
         *
4900
         * All scalar values (bool, float, int and string) are casted to string values before
4901
         * comparing to the given value. Non-scalar values in the map are ignored.
4902
         *
4903
         * @param string $value Value to compare map elements to
4904
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
4905
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
4906
         */
4907
        public function strCompare( string $value, bool $case = true ) : bool
4908
        {
4909
                $fcn = $case ? 'strcmp' : 'strcasecmp';
6✔
4910

4911
                foreach( $this->list() as $item )
6✔
4912
                {
4913
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
6✔
4914
                                return true;
6✔
4915
                        }
4916
                }
4917

4918
                return false;
6✔
4919
        }
4920

4921

4922
        /**
4923
         * Tests if at least one of the passed strings is part of at least one entry.
4924
         *
4925
         * Examples:
4926
         *  Map::from( ['abc'] )->strContains( '' );
4927
         *  Map::from( ['abc'] )->strContains( 'a' );
4928
         *  Map::from( ['abc'] )->strContains( 'bc' );
4929
         *  Map::from( [12345] )->strContains( '23' );
4930
         *  Map::from( [123.4] )->strContains( 23.4 );
4931
         *  Map::from( [12345] )->strContains( false );
4932
         *  Map::from( [12345] )->strContains( true );
4933
         *  Map::from( [false] )->strContains( false );
4934
         *  Map::from( [''] )->strContains( false );
4935
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4936
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4937
         *
4938
         *  Map::from( ['abc'] )->strContains( 'd' );
4939
         *  Map::from( ['abc'] )->strContains( 'cb' );
4940
         *  Map::from( [23456] )->strContains( true );
4941
         *  Map::from( [false] )->strContains( 0 );
4942
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4943
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4944
         *
4945
         * Results:
4946
         * The first eleven examples will return TRUE while the last six will return FALSE.
4947
         *
4948
         * @param array|string $value The string or list of strings to search for in each entry
4949
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4950
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4951
         * @todo 4.0 Add $case parameter at second position
4952
         */
4953
        public function strContains( $value, string $encoding = 'UTF-8' ) : bool
4954
        {
4955
                foreach( $this->list() as $entry )
3✔
4956
                {
4957
                        $entry = (string) $entry;
3✔
4958

4959
                        foreach( (array) $value as $str )
3✔
4960
                        {
4961
                                $str = (string) $str;
3✔
4962

4963
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
3✔
4964
                                        return true;
3✔
4965
                                }
4966
                        }
4967
                }
4968

4969
                return false;
3✔
4970
        }
4971

4972

4973
        /**
4974
         * Tests if all of the entries contains one of the passed strings.
4975
         *
4976
         * Examples:
4977
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
4978
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
4979
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
4980
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
4981
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
4982
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
4983
         *  Map::from( ['', false] )->strContainsAll( false );
4984
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
4985
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
4986
         *
4987
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
4988
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
4989
         *  Map::from( [23456, '123'] )->strContainsAll( true );
4990
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
4991
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
4992
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
4993
         *
4994
         * Results:
4995
         * The first nine examples will return TRUE while the last six will return FALSE.
4996
         *
4997
         * @param array|string $value The string or list of strings to search for in each entry
4998
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4999
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
5000
         * @todo 4.0 Add $case parameter at second position
5001
         */
5002
        public function strContainsAll( $value, string $encoding = 'UTF-8' ) : bool
5003
        {
5004
                $list = [];
3✔
5005

5006
                foreach( $this->list() as $entry )
3✔
5007
                {
5008
                        $entry = (string) $entry;
3✔
5009
                        $list[$entry] = 0;
3✔
5010

5011
                        foreach( (array) $value as $str )
3✔
5012
                        {
5013
                                $str = (string) $str;
3✔
5014

5015
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
3✔
5016
                                        $list[$entry] = 1; break;
3✔
5017
                                }
5018
                        }
5019
                }
5020

5021
                return array_sum( $list ) === count( $list );
3✔
5022
        }
5023

5024

5025
        /**
5026
         * Tests if at least one of the entries ends with one of the passed strings.
5027
         *
5028
         * Examples:
5029
         *  Map::from( ['abc'] )->strEnds( '' );
5030
         *  Map::from( ['abc'] )->strEnds( 'c' );
5031
         *  Map::from( ['abc'] )->strEnds( 'bc' );
5032
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
5033
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
5034
         *  Map::from( ['abc'] )->strEnds( 'a' );
5035
         *  Map::from( ['abc'] )->strEnds( 'cb' );
5036
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
5037
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
5038
         *
5039
         * Results:
5040
         * The first five examples will return TRUE while the last four will return FALSE.
5041
         *
5042
         * @param array|string $value The string or strings to search for in each entry
5043
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5044
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
5045
         * @todo 4.0 Add $case parameter at second position
5046
         */
5047
        public function strEnds( $value, string $encoding = 'UTF-8' ) : bool
5048
        {
5049
                foreach( $this->list() as $entry )
3✔
5050
                {
5051
                        $entry = (string) $entry;
3✔
5052

5053
                        foreach( (array) $value as $str )
3✔
5054
                        {
5055
                                $len = mb_strlen( (string) $str, $encoding );
3✔
5056

5057
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
3✔
5058
                                        return true;
3✔
5059
                                }
5060
                        }
5061
                }
5062

5063
                return false;
3✔
5064
        }
5065

5066

5067
        /**
5068
         * Tests if all of the entries ends with at least one of the passed strings.
5069
         *
5070
         * Examples:
5071
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
5072
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
5073
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
5074
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
5075
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
5076
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
5077
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
5078
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
5079
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
5080
         *
5081
         * Results:
5082
         * The first five examples will return TRUE while the last four will return FALSE.
5083
         *
5084
         * @param array|string $value The string or strings to search for in each entry
5085
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5086
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
5087
         * @todo 4.0 Add $case parameter at second position
5088
         */
5089
        public function strEndsAll( $value, string $encoding = 'UTF-8' ) : bool
5090
        {
5091
                $list = [];
3✔
5092

5093
                foreach( $this->list() as $entry )
3✔
5094
                {
5095
                        $entry = (string) $entry;
3✔
5096
                        $list[$entry] = 0;
3✔
5097

5098
                        foreach( (array) $value as $str )
3✔
5099
                        {
5100
                                $len = mb_strlen( (string) $str, $encoding );
3✔
5101

5102
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
3✔
5103
                                        $list[$entry] = 1; break;
3✔
5104
                                }
5105
                        }
5106
                }
5107

5108
                return array_sum( $list ) === count( $list );
3✔
5109
        }
5110

5111

5112
        /**
5113
         * Returns an element by key and casts it to string if possible.
5114
         *
5115
         * Examples:
5116
         *  Map::from( ['a' => true] )->string( 'a' );
5117
         *  Map::from( ['a' => 1] )->string( 'a' );
5118
         *  Map::from( ['a' => 1.1] )->string( 'a' );
5119
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
5120
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
5121
         *  Map::from( [] )->string( 'a', function() { return 'no'; } );
5122
         *
5123
         *  Map::from( [] )->string( 'b' );
5124
         *  Map::from( ['b' => ''] )->string( 'b' );
5125
         *  Map::from( ['b' => null] )->string( 'b' );
5126
         *  Map::from( ['b' => [true]] )->string( 'b' );
5127
         *  Map::from( ['b' => resource] )->string( 'b' );
5128
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
5129
         *
5130
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
5131
         *
5132
         * Results:
5133
         * The first six examples will return the value as string while the 9th to 12th
5134
         * example returns an empty string. The last example will throw an exception.
5135
         *
5136
         * This does also work for multi-dimensional arrays by passing the keys
5137
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5138
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5139
         * public properties of objects or objects implementing __isset() and __get() methods.
5140
         *
5141
         * @param int|string $key Key or path to the requested item
5142
         * @param mixed $default Default value if key isn't found (will be casted to bool)
5143
         * @return string Value from map or default value
5144
         */
5145
        public function string( $key, $default = '' ) : string
5146
        {
5147
                return (string) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
9✔
5148
        }
5149

5150

5151
        /**
5152
         * Converts all alphabetic characters in strings to lower case.
5153
         *
5154
         * Examples:
5155
         *  Map::from( ['My String'] )->strLower();
5156
         *  Map::from( ['Τάχιστη'] )->strLower();
5157
         *  Map::from( ['Äpfel', 'Birnen'] )->strLower( 'ISO-8859-1' );
5158
         *  Map::from( [123] )->strLower();
5159
         *  Map::from( [new stdClass] )->strLower();
5160
         *
5161
         * Results:
5162
         * The first example will return ["my string"], the second one ["τάχιστη"] and
5163
         * the third one ["äpfel", "birnen"]. The last two strings will be unchanged.
5164
         *
5165
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5166
         * @return self<int|string,mixed> Updated map for fluid interface
5167
         */
5168
        public function strLower( string $encoding = 'UTF-8' ) : self
5169
        {
5170
                foreach( $this->list() as &$entry )
3✔
5171
                {
5172
                        if( is_string( $entry ) ) {
3✔
5173
                                $entry = mb_strtolower( $entry, $encoding );
3✔
5174
                        }
5175
                }
5176

5177
                return $this;
3✔
5178
        }
5179

5180

5181
        /**
5182
         * Replaces all occurrences of the search string with the replacement string.
5183
         *
5184
         * Examples:
5185
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
5186
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
5187
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
5188
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
5189
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
5190
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
5191
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
5192
         *
5193
         * Restults:
5194
         * ['google.de', 'aimeos.de']
5195
         * ['google.de', 'aimeos.de']
5196
         * ['google.de', 'aimeos']
5197
         * ['google.fr', 'aimeos.de']
5198
         * ['google.de', 'aimeos.de']
5199
         * ['google.de', 'aimeos.de', 123]
5200
         * ['GOOGLE.de', 'AIMEOS.de']
5201
         *
5202
         * If you use an array of strings for search or search/replacement, the order of
5203
         * the strings matters! Each search string found is replaced by the corresponding
5204
         * replacement string at the same position.
5205
         *
5206
         * In case of array parameters and if the number of replacement strings is less
5207
         * than the number of search strings, the search strings with no corresponding
5208
         * replacement string are replaced with empty strings. Replacement strings with
5209
         * no corresponding search string are ignored.
5210
         *
5211
         * An array parameter for the replacements is only allowed if the search parameter
5212
         * is an array of strings too!
5213
         *
5214
         * Because the method replaces from left to right, it might replace a previously
5215
         * inserted value when doing multiple replacements. Entries which are non-string
5216
         * values are left untouched.
5217
         *
5218
         * @param array|string $search String or list of strings to search for
5219
         * @param array|string $replace String or list of strings of replacement strings
5220
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
5221
         * @return self<int|string,mixed> Updated map for fluid interface
5222
         */
5223
        public function strReplace( $search, $replace, bool $case = false ) : self
5224
        {
5225
                $fcn = $case ? 'str_ireplace' : 'str_replace';
3✔
5226

5227
                foreach( $this->list() as &$entry )
3✔
5228
                {
5229
                        if( is_string( $entry ) ) {
3✔
5230
                                $entry = $fcn( $search, $replace, $entry );
3✔
5231
                        }
5232
                }
5233

5234
                return $this;
3✔
5235
        }
5236

5237

5238
        /**
5239
         * Tests if at least one of the entries starts with at least one of the passed strings.
5240
         *
5241
         * Examples:
5242
         *  Map::from( ['abc'] )->strStarts( '' );
5243
         *  Map::from( ['abc'] )->strStarts( 'a' );
5244
         *  Map::from( ['abc'] )->strStarts( 'ab' );
5245
         *  Map::from( ['abc'] )->strStarts( ['a', 'b'] );
5246
         *  Map::from( ['abc'] )->strStarts( 'ab', 'ASCII' );
5247
         *  Map::from( ['abc'] )->strStarts( 'b' );
5248
         *  Map::from( ['abc'] )->strStarts( 'bc' );
5249
         *  Map::from( ['abc'] )->strStarts( ['b', 'c'] );
5250
         *  Map::from( ['abc'] )->strStarts( 'bc', 'ASCII' );
5251
         *
5252
         * Results:
5253
         * The first five examples will return TRUE while the last four will return FALSE.
5254
         *
5255
         * @param array|string $value The string or strings to search for in each entry
5256
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5257
         * @return bool TRUE if at least one of the entries starts with one of the strings, FALSE if not
5258
         * @todo 4.0 Add $case parameter at second position
5259
         */
5260
        public function strStarts( $value, string $encoding = 'UTF-8' ) : bool
5261
        {
5262
                foreach( $this->list() as $entry )
3✔
5263
                {
5264
                        $entry = (string) $entry;
3✔
5265

5266
                        foreach( (array) $value as $str )
3✔
5267
                        {
5268
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
3✔
5269
                                        return true;
3✔
5270
                                }
5271
                        }
5272
                }
5273

5274
                return false;
3✔
5275
        }
5276

5277

5278
        /**
5279
         * Tests if all of the entries starts with one of the passed strings.
5280
         *
5281
         * Examples:
5282
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
5283
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
5284
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
5285
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
5286
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
5287
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
5288
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
5289
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
5290
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
5291
         *
5292
         * Results:
5293
         * The first five examples will return TRUE while the last four will return FALSE.
5294
         *
5295
         * @param array|string $value The string or strings to search for in each entry
5296
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5297
         * @return bool TRUE if all of the entries start with at least one of the strings, FALSE if not
5298
         * @todo 4.0 Add $case parameter at second position
5299
         */
5300
        public function strStartsAll( $value, string $encoding = 'UTF-8' ) : bool
5301
        {
5302
                $list = [];
3✔
5303

5304
                foreach( $this->list() as $entry )
3✔
5305
                {
5306
                        $entry = (string) $entry;
3✔
5307
                        $list[$entry] = 0;
3✔
5308

5309
                        foreach( (array) $value as $str )
3✔
5310
                        {
5311
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
3✔
5312
                                        $list[$entry] = 1; break;
3✔
5313
                                }
5314
                        }
5315
                }
5316

5317
                return array_sum( $list ) === count( $list );
3✔
5318
        }
5319

5320

5321
        /**
5322
         * Converts all alphabetic characters in strings to upper case.
5323
         *
5324
         * Examples:
5325
         *  Map::from( ['My String'] )->strUpper();
5326
         *  Map::from( ['τάχιστη'] )->strUpper();
5327
         *  Map::from( ['äpfel', 'birnen'] )->strUpper( 'ISO-8859-1' );
5328
         *  Map::from( [123] )->strUpper();
5329
         *  Map::from( [new stdClass] )->strUpper();
5330
         *
5331
         * Results:
5332
         * The first example will return ["MY STRING"], the second one ["ΤΆΧΙΣΤΗ"] and
5333
         * the third one ["ÄPFEL", "BIRNEN"]. The last two strings will be unchanged.
5334
         *
5335
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5336
         * @return self<int|string,mixed> Updated map for fluid interface
5337
         */
5338
        public function strUpper( string $encoding = 'UTF-8' ) :self
5339
        {
5340
                foreach( $this->list() as &$entry )
3✔
5341
                {
5342
                        if( is_string( $entry ) ) {
3✔
5343
                                $entry = mb_strtoupper( $entry, $encoding );
3✔
5344
                        }
5345
                }
5346

5347
                return $this;
3✔
5348
        }
5349

5350

5351
        /**
5352
         * Adds a suffix at the end of each map entry.
5353
         *
5354
         * By default, nested arrays are walked recursively so all entries at all levels are suffixed.
5355
         *
5356
         * Examples:
5357
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
5358
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
5359
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
5360
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
5361
         *      return '-' . ( ord( $item ) + ord( $key ) );
5362
         *  } );
5363
         *
5364
         * Results:
5365
         *  The first example returns ['a-1', 'b-1'] while the second one will return
5366
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
5367
         *  level only so it will return ['a-1', ['b']]. The forth example passing
5368
         *  the closure will return ['a-145', 'b-147'].
5369
         *
5370
         * The keys are preserved using this method.
5371
         *
5372
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
5373
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
5374
         * @return self<int|string,mixed> Updated map for fluid interface
5375
         */
5376
        public function suffix( $suffix, ?int $depth = null ) : self
5377
        {
5378
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
3✔
5379

5380
                        foreach( $list as $key => $item )
3✔
5381
                        {
5382
                                if( is_array( $item ) ) {
3✔
5383
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
3✔
5384
                                } else {
5385
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
3✔
5386
                                }
5387
                        }
5388

5389
                        return $list;
3✔
5390
                };
3✔
5391

5392
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
3✔
5393
                return $this;
3✔
5394
        }
5395

5396

5397
        /**
5398
         * Returns the sum of all integer and float values in the map.
5399
         *
5400
         * Examples:
5401
         *  Map::from( [1, 3, 5] )->sum();
5402
         *  Map::from( [1, 'sum', 5] )->sum();
5403
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
5404
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
5405
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( fn( $val, $key ) => $val['i']['p'] ?? null )
5406
         *  Map::from( [30, 50, 10] )->sum( fn( $val, $key ) => $val < 50 ? $val : null )
5407
         *
5408
         * Results:
5409
         * The first line will return "9", the second one "6", the third one "90"
5410
         * the forth/fifth "80" and the last one "40".
5411
         *
5412
         * Non-numeric values will be removed before calculation.
5413
         *
5414
         * NULL values are treated as 0, non-numeric values will generate an error.
5415
         *
5416
         * This does also work for multi-dimensional arrays by passing the keys
5417
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5418
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5419
         * public properties of objects or objects implementing __isset() and __get() methods.
5420
         *
5421
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to sum up
5422
         * @return float Sum of all elements or 0 if there are no elements in the map
5423
         */
5424
        public function sum( $col = null ) : float
5425
        {
5426
                $list = $this->list();
9✔
5427
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, 'is_numeric' );
9✔
5428

5429
                return array_sum( $vals );
9✔
5430
        }
5431

5432

5433
        /**
5434
         * Returns a new map with the given number of items.
5435
         *
5436
         * The keys of the items returned in the new map are the same as in the original one.
5437
         *
5438
         * Examples:
5439
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
5440
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
5441
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
5442
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
5443
         *      return $item < 2;
5444
         *  } );
5445
         *
5446
         * Results:
5447
         *  [0 => 1, 1 => 2]
5448
         *  [1 => 2, 2 => 3]
5449
         *  [2 => 3, 3 => 4]
5450
         *  [1 => 2, 2 => 3]
5451
         *
5452
         * The keys of the items returned in the new map are the same as in the original one.
5453
         *
5454
         * @param int $size Number of items to return
5455
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5456
         * @return self<int|string,mixed> New map
5457
         */
5458
        public function take( int $size, $offset = 0 ) : self
5459
        {
5460
                $list = $this->list();
15✔
5461

5462
                if( is_numeric( $offset ) ) {
15✔
5463
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
9✔
5464
                }
5465

5466
                if( $offset instanceof \Closure ) {
6✔
5467
                        return new static( array_slice( $list, $this->until( $list, $offset ), $size, true ) );
3✔
5468
                }
5469

5470
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as second argument for take()' );
3✔
5471
        }
5472

5473

5474
        /**
5475
         * Passes a clone of the map to the given callback.
5476
         *
5477
         * Use it to "tap" into a chain of methods to check the state between two
5478
         * method calls. The original map is not altered by anything done in the
5479
         * callback.
5480
         *
5481
         * Examples:
5482
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
5483
         *    print_r( $map->remove( 0 )->toArray() );
5484
         *  } )->first();
5485
         *
5486
         * Results:
5487
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
5488
         * then prints the items without the first (`[2, 3]`) in the function passed
5489
         * to `tap()` and returns the first item ("1") at the end.
5490
         *
5491
         * @param callable $callback Function receiving ($map) parameter
5492
         * @return self<int|string,mixed> Same map for fluid interface
5493
         */
5494
        public function tap( callable $callback ) : self
5495
        {
5496
                $callback( clone $this );
3✔
5497
                return $this;
3✔
5498
        }
5499

5500

5501
        /**
5502
         * Returns the elements as a plain array.
5503
         *
5504
         * @return array<int|string,mixed> Plain array
5505
         */
5506
        public function to() : array
5507
        {
5508
                return $this->list = $this->array( $this->list );
3✔
5509
        }
5510

5511

5512
        /**
5513
         * Returns the elements as a plain array.
5514
         *
5515
         * @return array<int|string,mixed> Plain array
5516
         */
5517
        public function toArray() : array
5518
        {
5519
                return $this->list = $this->array( $this->list );
744✔
5520
        }
5521

5522

5523
        /**
5524
         * Returns the elements encoded as JSON string.
5525
         *
5526
         * There are several options available to modify the JSON output:
5527
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5528
         * The parameter can be a single JSON_* constant or a bitmask of several
5529
         * constants combine by bitwise OR (|), e.g.:
5530
         *
5531
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5532
         *
5533
         * @param int $options Combination of JSON_* constants
5534
         * @return string|null Array encoded as JSON string or NULL on failure
5535
         */
5536
        public function toJson( int $options = 0 ) : ?string
5537
        {
5538
                $result = json_encode( $this->list(), $options );
6✔
5539
                return $result !== false ? $result : null;
6✔
5540
        }
5541

5542

5543
        /**
5544
         * Reverses the element order in a copy of the map (alias).
5545
         *
5546
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5547
         * preferred because it uses one method call less than toReversed().
5548
         *
5549
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5550
         * @see reversed() - Underlying method with same parameters and return value but better performance
5551
         */
5552
        public function toReversed() : self
5553
        {
5554
                return $this->reversed();
3✔
5555
        }
5556

5557

5558
        /**
5559
         * Sorts the elements in a copy of the map using new keys (alias).
5560
         *
5561
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5562
         * preferred because it uses one method call less than toSorted().
5563
         *
5564
         * @param int $options Sort options for PHP sort()
5565
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5566
         * @see sorted() - Underlying method with same parameters and return value but better performance
5567
         */
5568
        public function toSorted( int $options = SORT_REGULAR ) : self
5569
        {
5570
                return $this->sorted( $options );
3✔
5571
        }
5572

5573

5574
        /**
5575
         * Creates a HTTP query string from the map elements.
5576
         *
5577
         * Examples:
5578
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5579
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5580
         *
5581
         * Results:
5582
         *  a=1&b=2
5583
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5584
         *
5585
         * @return string Parameter string for GET requests
5586
         */
5587
        public function toUrl() : string
5588
        {
5589
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
6✔
5590
        }
5591

5592

5593
        /**
5594
         * Creates new key/value pairs using the passed function and returns a new map for the result.
5595
         *
5596
         * Examples:
5597
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5598
         *      return [$key . '-2' => $value * 2];
5599
         *  } );
5600
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5601
         *      return [$key => $value * 2, $key . $key => $value * 4];
5602
         *  } );
5603
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5604
         *      return $key < 'b' ? [$key => $value * 2] : null;
5605
         *  } );
5606
         *  Map::from( ['la' => 2, 'le' => 4, 'li' => 6] )->transform( function( $value, $key ) {
5607
         *      return [$key[0] => $value * 2];
5608
         *  } );
5609
         *
5610
         * Results:
5611
         *  ['a-2' => 4, 'b-2' => 8]
5612
         *  ['a' => 4, 'aa' => 8, 'b' => 8, 'bb' => 16]
5613
         *  ['a' => 4]
5614
         *  ['l' => 12]
5615
         *
5616
         * If a key is returned twice, the last value will overwrite previous values.
5617
         *
5618
         * @param \Closure $callback Function with (value, key) parameters and returns an array of new key/value pair(s)
5619
         * @return self<int|string,mixed> New map with the new key/value pairs
5620
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
5621
         * @see rekey() - Changes the keys according to the passed function
5622
         */
5623
        public function transform( \Closure $callback ) : self
5624
        {
5625
                $result = [];
12✔
5626

5627
                foreach( $this->list() as $key => $value )
12✔
5628
                {
5629
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
12✔
5630
                                $result[$newkey] = $newval;
12✔
5631
                        }
5632
                }
5633

5634
                return new static( $result );
12✔
5635
        }
5636

5637

5638
        /**
5639
         * Exchanges rows and columns for a two dimensional map.
5640
         *
5641
         * Examples:
5642
         *  Map::from( [
5643
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5644
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5645
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5646
         *  ] )->transpose();
5647
         *
5648
         *  Map::from( [
5649
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5650
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5651
         *    ['name' => 'C', 2020 => 400]
5652
         *  ] );
5653
         *
5654
         * Results:
5655
         *  [
5656
         *    'name' => ['A', 'B', 'C'],
5657
         *    2020 => [200, 300, 400],
5658
         *    2021 => [100, 200, 300],
5659
         *    2022 => [50, 100, 200]
5660
         *  ]
5661
         *
5662
         *  [
5663
         *    'name' => ['A', 'B', 'C'],
5664
         *    2020 => [200, 300, 400],
5665
         *    2021 => [100, 200],
5666
         *    2022 => [50]
5667
         *  ]
5668
         *
5669
         * @return self<int|string,mixed> New map
5670
         */
5671
        public function transpose() : self
5672
        {
5673
                $result = [];
6✔
5674

5675
                foreach( (array) $this->first( [] ) as $key => $col ) {
6✔
5676
                        $result[$key] = array_column( $this->list(), $key );
6✔
5677
                }
5678

5679
                return new static( $result );
6✔
5680
        }
5681

5682

5683
        /**
5684
         * Traverses trees of nested items passing each item to the callback.
5685
         *
5686
         * This does work for nested arrays and objects with public properties or
5687
         * objects implementing __isset() and __get() methods. To build trees
5688
         * of nested items, use the tree() method.
5689
         *
5690
         * Examples:
5691
         *   Map::from( [[
5692
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5693
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5694
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5695
         *     ]
5696
         *   ]] )->traverse();
5697
         *
5698
         *   Map::from( [[
5699
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5700
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5701
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5702
         *     ]
5703
         *   ]] )->traverse( function( $entry, $key, $level, $parent ) {
5704
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
5705
         *   } );
5706
         *
5707
         *   Map::from( [[
5708
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5709
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5710
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5711
         *     ]
5712
         *   ]] )->traverse( function( &$entry, $key, $level, $parent ) {
5713
         *     $entry['path'] = isset( $parent['path'] ) ? $parent['path'] . '/' . $entry['name'] : $entry['name'];
5714
         *     return $entry;
5715
         *   } );
5716
         *
5717
         *   Map::from( [[
5718
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
5719
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
5720
         *     ]
5721
         *   ]] )->traverse( null, 'nodes' );
5722
         *
5723
         * Results:
5724
         *   [
5725
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
5726
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5727
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
5728
         *   ]
5729
         *
5730
         *   ['- n1', '-- n2', '-- n3']
5731
         *
5732
         *   [
5733
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...], 'path' => 'n1'],
5734
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => [], 'path' => 'n1/n2'],
5735
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => [], 'path' => 'n1/n3'],
5736
         *   ]
5737
         *
5738
         *   [
5739
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
5740
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
5741
         *   ]
5742
         *
5743
         * @param \Closure|null $callback Callback with (entry, key, level, $parent) arguments, returns the entry added to result
5744
         * @param string $nestKey Key to the children of each item
5745
         * @return self<int|string,mixed> New map with all items as flat list
5746
         */
5747
        public function traverse( ?\Closure $callback = null, string $nestKey = 'children' ) : self
5748
        {
5749
                $result = [];
15✔
5750
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
15✔
5751

5752
                return map( $result );
15✔
5753
        }
5754

5755

5756
        /**
5757
         * Creates a tree structure from the list items.
5758
         *
5759
         * Use this method to rebuild trees e.g. from database records. To traverse
5760
         * trees, use the traverse() method.
5761
         *
5762
         * Examples:
5763
         *  Map::from( [
5764
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
5765
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
5766
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
5767
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
5768
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
5769
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
5770
         *  ] )->tree( 'id', 'pid' );
5771
         *
5772
         * Results:
5773
         *   [1 => [
5774
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
5775
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
5776
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
5777
         *       ]],
5778
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
5779
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
5780
         *       ]],
5781
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
5782
         *     ]
5783
         *   ]]
5784
         *
5785
         * To build the tree correctly, the items must be in order or at least the
5786
         * nodes of the lower levels must come first. For a tree like this:
5787
         * n1
5788
         * |- n2
5789
         * |  |- n3
5790
         * |- n4
5791
         * |  |- n5
5792
         * |- n6
5793
         *
5794
         * Accepted item order:
5795
         * - in order: n1, n2, n3, n4, n5, n6
5796
         * - lower levels first: n1, n2, n4, n6, n3, n5
5797
         *
5798
         * If your items are unordered, apply usort() first to the map entries, e.g.
5799
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
5800
         *     return $item1['lvl'] <=> $item2['lvl'];
5801
         *   } );
5802
         *
5803
         * @param string $idKey Name of the key with the unique ID of the node
5804
         * @param string $parentKey Name of the key with the ID of the parent node
5805
         * @param string $nestKey Name of the key with will contain the children of the node
5806
         * @return self<int|string,mixed> New map with one or more root tree nodes
5807
         */
5808
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
5809
        {
5810
                $this->list();
3✔
5811
                $trees = $refs = [];
3✔
5812

5813
                foreach( $this->list as &$node )
3✔
5814
                {
5815
                        $node[$nestKey] = [];
3✔
5816
                        $refs[$node[$idKey]] = &$node;
3✔
5817

5818
                        if( $node[$parentKey] ) {
3✔
5819
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
3✔
5820
                        } else {
5821
                                $trees[$node[$idKey]] = &$node;
3✔
5822
                        }
5823
                }
5824

5825
                return map( $trees );
3✔
5826
        }
5827

5828

5829
        /**
5830
         * Removes the passed characters from the left/right of all strings.
5831
         *
5832
         * Examples:
5833
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5834
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5835
         *
5836
         * Results:
5837
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5838
         *
5839
         * @param string $chars List of characters to trim
5840
         * @return self<int|string,mixed> Updated map for fluid interface
5841
         */
5842
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5843
        {
5844
                foreach( $this->list() as &$entry )
3✔
5845
                {
5846
                        if( is_string( $entry ) ) {
3✔
5847
                                $entry = trim( $entry, $chars );
3✔
5848
                        }
5849
                }
5850

5851
                return $this;
3✔
5852
        }
5853

5854

5855
        /**
5856
         * Sorts all elements using a callback and maintains the key association.
5857
         *
5858
         * The given callback will be used to compare the values. The callback must accept
5859
         * two parameters (item A and B) and must return -1 if item A is smaller than
5860
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5861
         * method name and an anonymous function can be passed.
5862
         *
5863
         * Examples:
5864
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( 'strcasecmp' );
5865
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( function( $itemA, $itemB ) {
5866
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5867
         *  } );
5868
         *
5869
         * Results:
5870
         *  ['b' => 'a', 'a' => 'B']
5871
         *  ['b' => 'a', 'a' => 'B']
5872
         *
5873
         * The keys are preserved using this method and no new map is created.
5874
         *
5875
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5876
         * @return self<int|string,mixed> Updated map for fluid interface
5877
         */
5878
        public function uasort( callable $callback ) : self
5879
        {
5880
                uasort( $this->list(), $callback );
6✔
5881
                return $this;
6✔
5882
        }
5883

5884

5885
        /**
5886
         * Sorts all elements using a callback and maintains the key association.
5887
         *
5888
         * The given callback will be used to compare the values. The callback must accept
5889
         * two parameters (item A and B) and must return -1 if item A is smaller than
5890
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5891
         * method name and an anonymous function can be passed.
5892
         *
5893
         * Examples:
5894
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( 'strcasecmp' );
5895
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( function( $itemA, $itemB ) {
5896
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5897
         *  } );
5898
         *
5899
         * Results:
5900
         *  ['b' => 'a', 'a' => 'B']
5901
         *  ['b' => 'a', 'a' => 'B']
5902
         *
5903
         * The keys are preserved using this method and a new map is created.
5904
         *
5905
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5906
         * @return self<int|string,mixed> New map with sorted elements
5907
         */
5908
        public function uasorted( callable $callback ) : self
5909
        {
5910
                return ( clone $this )->uasort( $callback );
3✔
5911
        }
5912

5913

5914
        /**
5915
         * Sorts the map elements by their keys using a callback.
5916
         *
5917
         * The given callback will be used to compare the keys. The callback must accept
5918
         * two parameters (key A and B) and must return -1 if key A is smaller than
5919
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5920
         * method name and an anonymous function can be passed.
5921
         *
5922
         * Examples:
5923
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( 'strcasecmp' );
5924
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( function( $keyA, $keyB ) {
5925
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5926
         *  } );
5927
         *
5928
         * Results:
5929
         *  ['a' => 'b', 'B' => 'a']
5930
         *  ['a' => 'b', 'B' => 'a']
5931
         *
5932
         * The keys are preserved using this method and no new map is created.
5933
         *
5934
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5935
         * @return self<int|string,mixed> Updated map for fluid interface
5936
         */
5937
        public function uksort( callable $callback ) : self
5938
        {
5939
                uksort( $this->list(), $callback );
6✔
5940
                return $this;
6✔
5941
        }
5942

5943

5944
        /**
5945
         * Sorts a copy of the map elements by their keys using a callback.
5946
         *
5947
         * The given callback will be used to compare the keys. The callback must accept
5948
         * two parameters (key A and B) and must return -1 if key A is smaller than
5949
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5950
         * method name and an anonymous function can be passed.
5951
         *
5952
         * Examples:
5953
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5954
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5955
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5956
         *  } );
5957
         *
5958
         * Results:
5959
         *  ['a' => 'b', 'B' => 'a']
5960
         *  ['a' => 'b', 'B' => 'a']
5961
         *
5962
         * The keys are preserved using this method and a new map is created.
5963
         *
5964
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5965
         * @return self<int|string,mixed> New map with sorted elements
5966
         */
5967
        public function uksorted( callable $callback ) : self
5968
        {
5969
                return ( clone $this )->uksort( $callback );
3✔
5970
        }
5971

5972

5973
        /**
5974
         * Unflattens the key path/value pairs into a multi-dimensional array.
5975
         *
5976
         * Examples:
5977
         *  Map::from( ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3] )->unflatten();
5978
         *  Map::from( ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3] )->sep( '.' )->unflatten();
5979
         *
5980
         * Results:
5981
         * ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]]
5982
         *
5983
         * This is the inverse method for flatten().
5984
         *
5985
         * @return self<int|string,mixed> New map with multi-dimensional arrays
5986
         */
5987
        public function unflatten() : self
5988
        {
5989
                $result = [];
3✔
5990

5991
                foreach( $this->list() as $key => $value )
3✔
5992
                {
5993
                        $nested = &$result;
3✔
5994
                        $parts = explode( $this->sep, $key );
3✔
5995

5996
                        while( count( $parts ) > 1 ) {
3✔
5997
                                $nested = &$nested[array_shift( $parts )] ?? [];
3✔
5998
                        }
5999

6000
                        $nested[array_shift( $parts )] = $value;
3✔
6001
                }
6002

6003
                return new static( $result );
3✔
6004
        }
6005

6006

6007
        /**
6008
         * Builds a union of the elements and the given elements without overwriting existing ones.
6009
         * Existing keys in the map will not be overwritten
6010
         *
6011
         * Examples:
6012
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
6013
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
6014
         *
6015
         * Results:
6016
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
6017
         * isn't overwritten. In the second example, the result will be a combined
6018
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
6019
         *
6020
         * If list entries should be overwritten,  please use merge() instead!
6021
         * The keys are preserved using this method and no new map is created.
6022
         *
6023
         * @param iterable<int|string,mixed> $elements List of elements
6024
         * @return self<int|string,mixed> Updated map for fluid interface
6025
         */
6026
        public function union( iterable $elements ) : self
6027
        {
6028
                $this->list = $this->list() + $this->array( $elements );
6✔
6029
                return $this;
6✔
6030
        }
6031

6032

6033
        /**
6034
         * Returns only unique elements from the map incl. their keys.
6035
         *
6036
         * Examples:
6037
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
6038
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
6039
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
6040
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( fn( $item, $key ) => $item['i']['p'] )
6041
         *
6042
         * Results:
6043
         * [0 => 'a', 1 => 'b', 3 => 'c']
6044
         * [['p' => 1], ['p' => 2]]
6045
         * [['i' => ['p' => '1']]]
6046
         * [['i' => ['p' => '1']]]
6047
         *
6048
         * Two elements are considered equal if comparing their string representions returns TRUE:
6049
         * (string) $elem1 === (string) $elem2
6050
         *
6051
         * The keys of the elements are only preserved in the new map if no key is passed.
6052
         *
6053
         * @param \Closure|string|null $col Key, path of the nested array or anonymous function with ($item, $key) parameters returning the value for comparison
6054
         * @return self<int|string,mixed> New map
6055
         */
6056
        public function unique( $col = null ) : self
6057
        {
6058
                if( $col === null ) {
15✔
6059
                        return new static( array_unique( $this->list() ) );
6✔
6060
                }
6061

6062
                $list = $this->list();
9✔
6063
                $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
9✔
6064

6065
                return new static( array_intersect_key( $list, array_unique( $map ) ) );
9✔
6066
        }
6067

6068

6069
        /**
6070
         * Pushes an element onto the beginning of the map without returning a new map.
6071
         *
6072
         * Examples:
6073
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
6074
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
6075
         *
6076
         * Results:
6077
         *  ['d', 'a', 'b']
6078
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
6079
         *
6080
         * The keys of the elements are only preserved in the new map if no key is passed.
6081
         *
6082
         * Performance note:
6083
         * The bigger the list, the higher the performance impact because unshift()
6084
         * needs to create a new list and copies all existing elements to the new
6085
         * array. Usually, it's better to push() new entries at the end and reverse()
6086
         * the list afterwards:
6087
         *
6088
         *  $map->push( 'a' )->push( 'b' )->reverse();
6089
         * instead of
6090
         *  $map->unshift( 'a' )->unshift( 'b' );
6091
         *
6092
         * @param mixed $value Item to add at the beginning
6093
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
6094
         * @return self<int|string,mixed> Updated map for fluid interface
6095
         */
6096
        public function unshift( $value, $key = null ) : self
6097
        {
6098
                if( $key === null ) {
9✔
6099
                        array_unshift( $this->list(), $value );
6✔
6100
                } else {
6101
                        $this->list = [$key => $value] + $this->list();
3✔
6102
                }
6103

6104
                return $this;
9✔
6105
        }
6106

6107

6108
        /**
6109
         * Sorts all elements using a callback using new keys.
6110
         *
6111
         * The given callback will be used to compare the values. The callback must accept
6112
         * two parameters (item A and B) and must return -1 if item A is smaller than
6113
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
6114
         * method name and an anonymous function can be passed.
6115
         *
6116
         * Examples:
6117
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( 'strcasecmp' );
6118
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( function( $itemA, $itemB ) {
6119
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
6120
         *  } );
6121
         *
6122
         * Results:
6123
         *  [0 => 'a', 1 => 'B']
6124
         *  [0 => 'a', 1 => 'B']
6125
         *
6126
         * The keys aren't preserved and elements get a new index. No new map is created.
6127
         *
6128
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
6129
         * @return self<int|string,mixed> Updated map for fluid interface
6130
         */
6131
        public function usort( callable $callback ) : self
6132
        {
6133
                usort( $this->list(), $callback );
6✔
6134
                return $this;
6✔
6135
        }
6136

6137

6138
        /**
6139
         * Sorts a copy of all elements using a callback using new keys.
6140
         *
6141
         * The given callback will be used to compare the values. The callback must accept
6142
         * two parameters (item A and B) and must return -1 if item A is smaller than
6143
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
6144
         * method name and an anonymous function can be passed.
6145
         *
6146
         * Examples:
6147
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
6148
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
6149
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
6150
         *  } );
6151
         *
6152
         * Results:
6153
         *  [0 => 'a', 1 => 'B']
6154
         *  [0 => 'a', 1 => 'B']
6155
         *
6156
         * The keys aren't preserved, elements get a new index and a new map is created.
6157
         *
6158
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
6159
         * @return self<int|string,mixed> New map with sorted elements
6160
         */
6161
        public function usorted( callable $callback ) : self
6162
        {
6163
                return ( clone $this )->usort( $callback );
3✔
6164
        }
6165

6166

6167
        /**
6168
         * Resets the keys and return the values in a new map.
6169
         *
6170
         * Examples:
6171
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
6172
         *
6173
         * Results:
6174
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
6175
         *
6176
         * @return self<int|string,mixed> New map of the values
6177
         */
6178
        public function values() : self
6179
        {
6180
                return new static( array_values( $this->list() ) );
30✔
6181
        }
6182

6183

6184
        /**
6185
         * Applies the given callback to all elements.
6186
         *
6187
         * To change the values of the Map, specify the value parameter as reference
6188
         * (&$value). You can only change the values but not the keys nor the array
6189
         * structure.
6190
         *
6191
         * Examples:
6192
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
6193
         *    $value = strtoupper( $value );
6194
         *  } );
6195
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
6196
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
6197
         *  } );
6198
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
6199
         *    $value = $data[$value] ?? $value;
6200
         *  }, [1 => 'one', 2 => 'two'] );
6201
         *
6202
         * Results:
6203
         * The first example will change the Map elements to:
6204
         *   ['A', 'B', ['C', 'D'], 'E']
6205
         * The output of the second one will be:
6206
         *  ASCII 66 is B
6207
         *  ASCII 97 is a
6208
         * The last example changes the Map elements to:
6209
         *  ['one', 'two', 3]
6210
         *
6211
         * By default, Map elements which are arrays will be traversed recursively.
6212
         * To iterate over the Map elements only, pass FALSE as third parameter.
6213
         *
6214
         * @param callable $callback Function with (item, key, data) parameters
6215
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
6216
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
6217
         * @return self<int|string,mixed> Updated map for fluid interface
6218
         */
6219
        public function walk( callable $callback, $data = null, bool $recursive = true ) : self
6220
        {
6221
                if( $recursive ) {
9✔
6222
                        array_walk_recursive( $this->list(), $callback, $data );
6✔
6223
                } else {
6224
                        array_walk( $this->list(), $callback, $data );
3✔
6225
                }
6226

6227
                return $this;
9✔
6228
        }
6229

6230

6231
        /**
6232
         * Filters the list of elements by a given condition.
6233
         *
6234
         * Examples:
6235
         *  Map::from( [
6236
         *    ['id' => 1, 'type' => 'name'],
6237
         *    ['id' => 2, 'type' => 'short'],
6238
         *  ] )->where( 'type', '==', 'name' );
6239
         *
6240
         *  Map::from( [
6241
         *    ['id' => 3, 'price' => 10],
6242
         *    ['id' => 4, 'price' => 50],
6243
         *  ] )->where( 'price', '>', 20 );
6244
         *
6245
         *  Map::from( [
6246
         *    ['id' => 3, 'price' => 10],
6247
         *    ['id' => 4, 'price' => 50],
6248
         *  ] )->where( 'price', 'in', [10, 25] );
6249
         *
6250
         *  Map::from( [
6251
         *    ['id' => 3, 'price' => 10],
6252
         *    ['id' => 4, 'price' => 50],
6253
         *  ] )->where( 'price', '-', [10, 100] );
6254
         *
6255
         *  Map::from( [
6256
         *    ['item' => ['id' => 3, 'price' => 10]],
6257
         *    ['item' => ['id' => 4, 'price' => 50]],
6258
         *  ] )->where( 'item/price', '>', 30 );
6259
         *
6260
         * Results:
6261
         *  [0 => ['id' => 1, 'type' => 'name']]
6262
         *  [1 => ['id' => 4, 'price' => 50]]
6263
         *  [0 => ['id' => 3, 'price' => 10]]
6264
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
6265
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
6266
         *
6267
         * Available operators are:
6268
         * * '==' : Equal
6269
         * * '===' : Equal and same type
6270
         * * '!=' : Not equal
6271
         * * '!==' : Not equal and same type
6272
         * * '<=' : Smaller than an equal
6273
         * * '>=' : Greater than an equal
6274
         * * '<' : Smaller
6275
         * * '>' : Greater
6276
         * 'in' : Array of value which are in the list of values
6277
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
6278
         *
6279
         * This does also work for multi-dimensional arrays by passing the keys
6280
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
6281
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
6282
         * public properties of objects or objects implementing __isset() and __get() methods.
6283
         *
6284
         * The keys of the original map are preserved in the returned map.
6285
         *
6286
         * @param string $key Key or path of the value in the array or object used for comparison
6287
         * @param string $op Operator used for comparison
6288
         * @param mixed $value Value used for comparison
6289
         * @return self<int|string,mixed> New map for fluid interface
6290
         */
6291
        public function where( string $key, string $op, $value ) : self
6292
        {
6293
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
18✔
6294

6295
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
18✔
6296
                        {
6297
                                switch( $op )
6298
                                {
6299
                                        case '-':
15✔
6300
                                                $list = (array) $value;
3✔
6301
                                                return !empty( $list ) && $val >= reset( $list ) && $val <= end( $list );
3✔
6302
                                        case 'in': return in_array( $val, (array) $value );
12✔
6303
                                        case '<': return $val < $value;
9✔
6304
                                        case '>': return $val > $value;
9✔
6305
                                        case '<=': return $val <= $value;
6✔
6306
                                        case '>=': return $val >= $value;
6✔
6307
                                        case '===': return $val === $value;
6✔
6308
                                        case '!==': return $val !== $value;
6✔
6309
                                        case '!=': return $val != $value;
6✔
6310
                                        default: return $val == $value;
6✔
6311
                                }
6312
                        }
6313

6314
                        return false;
3✔
6315
                } );
18✔
6316
        }
6317

6318

6319
        /**
6320
         * Returns a copy of the map with the element at the given index replaced with the given value.
6321
         *
6322
         * Examples:
6323
         *  $m = Map::from( ['a' => 1] );
6324
         *  $m->with( 2, 'b' );
6325
         *  $m->with( 'a', 2 );
6326
         *
6327
         * Results:
6328
         *  ['a' => 1, 2 => 'b']
6329
         *  ['a' => 2]
6330
         *
6331
         * The original map ($m) stays untouched!
6332
         * This method is a shortcut for calling the copy() and set() methods.
6333
         *
6334
         * @param int|string $key Array key to set or replace
6335
         * @param mixed $value New value for the given key
6336
         * @return self<int|string,mixed> New map
6337
         */
6338
        public function with( $key, $value ) : self
6339
        {
6340
                return ( clone $this )->set( $key, $value );
3✔
6341
        }
6342

6343

6344
        /**
6345
         * Merges the values of all arrays at the corresponding index.
6346
         *
6347
         * Examples:
6348
         *  $en = ['one', 'two', 'three'];
6349
         *  $es = ['uno', 'dos', 'tres'];
6350
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
6351
         *
6352
         * Results:
6353
         *  [
6354
         *    [1, 'one', 'uno'],
6355
         *    [2, 'two', 'dos'],
6356
         *    [3, 'three', 'tres'],
6357
         *  ]
6358
         *
6359
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
6360
         * @return self<int|string,mixed> New map of arrays
6361
         */
6362
        public function zip( ...$arrays ) : self
6363
        {
6364
                $args = array_map( function( $items ) {
3✔
6365
                        return $this->array( $items );
3✔
6366
                }, $arrays );
3✔
6367

6368
                return new static( array_map( null, $this->list(), ...$args ) );
3✔
6369
        }
6370

6371

6372
        /**
6373
         * Returns a plain array of the given elements.
6374
         *
6375
         * @param mixed $elements List of elements or single value
6376
         * @return array<int|string,mixed> Plain array
6377
         */
6378
        protected function array( $elements ) : array
6379
        {
6380
                if( is_array( $elements ) ) {
789✔
6381
                        return $elements;
759✔
6382
                }
6383

6384
                if( $elements instanceof \Closure ) {
117✔
6385
                        return (array) $elements();
×
6386
                }
6387

6388
                if( $elements instanceof \Aimeos\Map ) {
117✔
6389
                        return $elements->toArray();
69✔
6390
                }
6391

6392
                if( is_iterable( $elements ) ) {
51✔
6393
                        return iterator_to_array( $elements, true );
9✔
6394
                }
6395

6396
                return $elements !== null ? [$elements] : [];
42✔
6397
        }
6398

6399

6400
        /**
6401
         * Flattens a multi-dimensional array or map into a single level array.
6402
         *
6403
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6404
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
6405
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6406
         */
6407
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
6408
        {
6409
                foreach( $entries as $key => $entry )
15✔
6410
                {
6411
                        if( is_iterable( $entry ) && $depth > 0 ) {
15✔
6412
                                $this->kflatten( $entry, $result, $depth - 1 );
15✔
6413
                        } else {
6414
                                $result[$key] = $entry;
15✔
6415
                        }
6416
                }
6417
        }
6418

6419

6420
        /**
6421
         * Returns a reference to the array of elements
6422
         *
6423
         * @return array Reference to the array of elements
6424
         */
6425
        protected function &list() : array
6426
        {
6427
                if( !is_array( $this->list ) ) {
1,128✔
6428
                        $this->list = $this->array( $this->list );
×
6429
                }
6430

6431
                return $this->list;
1,128✔
6432
        }
6433

6434

6435
        /**
6436
         * Returns a closure that retrieves the value for the passed key
6437
         *
6438
         * @param \Closure|string|null $key Closure or key (e.g. "key1/key2/key3") to retrieve the value for
6439
         * @return \Closure Closure that retrieves the value for the passed key
6440
         */
6441
        protected function mapper( $key = null ) : \Closure
6442
        {
6443
                if( $key instanceof \Closure ) {
42✔
6444
                        return $key;
18✔
6445
                }
6446

6447
                $parts = $key ? explode( $this->sep, (string) $key ) : [];
24✔
6448

6449
                return function( $item ) use ( $parts ) {
24✔
6450
                        return $this->val( $item, $parts );
24✔
6451
                };
24✔
6452
        }
6453

6454

6455
        /**
6456
         * Flattens a multi-dimensional array or map into a single level array.
6457
         *
6458
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6459
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
6460
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6461
         */
6462
        protected function nflatten( iterable $entries, array &$result, int $depth ) : void
6463
        {
6464
                foreach( $entries as $entry )
15✔
6465
                {
6466
                        if( is_iterable( $entry ) && $depth > 0 ) {
15✔
6467
                                $this->nflatten( $entry, $result, $depth - 1 );
12✔
6468
                        } else {
6469
                                $result[] = $entry;
15✔
6470
                        }
6471
                }
6472
        }
6473

6474

6475
        /**
6476
         * Flattens a multi-dimensional array or map into an array with joined keys.
6477
         *
6478
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6479
         * @param array<int|string,mixed> $result Will contain joined key/value pairs from the multi-dimensional arrays afterwards
6480
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6481
         * @param string $path Path prefix of the current key
6482
         */
6483
        protected function rflatten( iterable $entries, array &$result, int $depth, string $path = '' ) : void
6484
        {
6485
                foreach( $entries as $key => $entry )
3✔
6486
                {
6487
                        if( is_iterable( $entry ) && $depth > 0 ) {
3✔
6488
                                $this->rflatten( $entry, $result, $depth - 1, $path . $key . $this->sep );
3✔
6489
                        } else {
6490
                                $result[$path . $key] = $entry;
3✔
6491
                        }
6492
                }
6493
        }
6494

6495

6496
        /**
6497
         * Returns the position of the first element that doesn't match the condition
6498
         *
6499
         * @param iterable<int|string,mixed> $list List of elements to check
6500
         * @param \Closure $callback Closure with ($item, $key) arguments to check the condition
6501
         * @return int Position of the first element that doesn't match the condition
6502
         */
6503
        protected function until( iterable $list, \Closure $callback ) : int
6504
        {
6505
                $idx = 0;
6✔
6506

6507
                foreach( $list as $key => $item )
6✔
6508
                {
6509
                        if( !$callback( $item, $key ) ) {
6✔
6510
                                break;
6✔
6511
                        }
6512

6513
                        ++$idx;
6✔
6514
                }
6515

6516
                return $idx;
6✔
6517
        }
6518

6519

6520
        /**
6521
         * Returns a configuration value from an array.
6522
         *
6523
         * @param array<mixed>|object $entry The array or object to look at
6524
         * @param array<string> $parts Path parts to look for inside the array or object
6525
         * @return mixed Found value or null if no value is available
6526
         */
6527
        protected function val( $entry, array $parts )
6528
        {
6529
                foreach( $parts as $part )
141✔
6530
                {
6531
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
135✔
6532
                                $entry = $entry[$part];
78✔
6533
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
81✔
6534
                                $entry = $entry->{$part};
6✔
6535
                        } else {
6536
                                return null;
75✔
6537
                        }
6538
                }
6539

6540
                return $entry;
87✔
6541
        }
6542

6543

6544
        /**
6545
         * Visits each entry, calls the callback and returns the items in the result argument
6546
         *
6547
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
6548
         * @param array<mixed> $result Numerically indexed list of all visited entries
6549
         * @param int $level Current depth of the nodes in the tree
6550
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
6551
         * @param string $nestKey Key to the children of each entry
6552
         * @param array<mixed>|object|null $parent Parent entry
6553
         */
6554
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, $parent = null ) : void
6555
        {
6556
                foreach( $entries as $key => $entry )
15✔
6557
                {
6558
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
15✔
6559

6560
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
15✔
6561
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
12✔
6562
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
3✔
6563
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
3✔
6564
                        }
6565
                }
6566
        }
6567
}
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