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

aimeos / map / 21901339665

11 Feb 2026 10:24AM UTC coverage: 97.847%. Remained the same
21901339665

push

github

aimeos
Fixed documentation

818 of 836 relevant lines covered (97.85%)

62.42 hits per line

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

97.84
/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;
4,200✔
53
                $this->list = $elements;
4,200✔
54
        }
840✔
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] ) ) {
20✔
75
                        throw new \BadMethodCallException( sprintf( 'Method %s::%s does not exist.', static::class, $name ) );
10✔
76
                }
77

78
                return call_user_func_array( \Closure::bind( static::$methods[$name], null, static::class ), $params );
10✔
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] ) ) {
40✔
114
                        return call_user_func_array( static::$methods[$name]->bindTo( $this, static::class ), $params );
20✔
115
                }
116

117
                $result = [];
20✔
118

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

126
                return new static( $result );
20✔
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 );
10✔
138
        }
139

140

141
        /**
142
         * Sets or returns the seperator 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;
10✔
162

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

167
                return $old;
10✔
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 !== '' ) {
80✔
206
                        return new static( explode( $delimiter, $string, $limit ) );
40✔
207
                }
208

209
                $limit = $limit ?: 1;
40✔
210
                $parts = mb_str_split( $string );
40✔
211

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

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

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

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

227

228
        /**
229
         * Creates a new map filled with given value.
230
         *
231
         * Exapmles:
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 ) );
20✔
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 ) {
2,020✔
273
                        return $elements;
30✔
274
                }
275

276
                return new static( $elements );
2,020✔
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 int $options Combination of JSON_* constants
305
         * @return self<int|string,mixed> New map from decoded JSON string
306
         * @throws \RuntimeException If the passed JSON string is invalid
307
         */
308
        public static function fromJson( string $json, int $options = JSON_BIGINT_AS_STRING ) : self
309
        {
310
                if( ( $result = json_decode( $json, true, 512, $options ) ) !== null ) {
40✔
311
                        return new static( $result );
30✔
312
                }
313

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

317

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

344
                return self::$methods[$method] ?? null;
30✔
345
        }
346

347

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

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

385
                return new static( $list );
30✔
386
        }
387

388

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

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

420

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

431

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

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

462
                return false;
8✔
463
        }
464

465

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

500

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

534

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

569

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

603

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

630

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

662
                return !empty( $vals ) ? array_sum( $vals ) / count( $vals ) : 0;
30✔
663
        }
664

665

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

693

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

734

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

761
                foreach( $this->list() as $key => $item )
10✔
762
                {
763
                        if( is_object( $item ) ) {
10✔
764
                                $result[$key] = $item->{$name}( ...$params );
10✔
765
                        }
766
                }
767

768
                return new static( $result );
10✔
769
        }
770

771

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

812
                return $this;
10✔
813
        }
814

815

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

843
                return new static( array_chunk( $this->list(), $size, $preserve ) );
20✔
844
        }
845

846

847
        /**
848
         * Removes all elements from the current map.
849
         *
850
         * @return self<int|string,mixed> Updated map for fluid interface
851
         */
852
        public function clear() : self
853
        {
854
                $this->list = [];
40✔
855
                return $this;
40✔
856
        }
857

858

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

879
                foreach( $this->list() as $key => $item ) {
10✔
880
                        $list[$key] = is_object( $item ) ? clone $item : $item;
10✔
881
                }
882

883
                return new static( $list );
10✔
884
        }
885

886

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

926
                if( ( $valuecol === null || count( $vparts ) === 1 )
90✔
927
                        && ( $indexcol === null || count( $iparts ) === 1 )
90✔
928
                ) {
929
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
60✔
930
                }
931

932
                $list = [];
30✔
933

934
                foreach( $this->list() as $key => $item )
30✔
935
                {
936
                        $v = $valuecol ? $this->val( $item, $vparts ) : $item;
30✔
937

938
                        if( $indexcol && ( $k = (string) $this->val( $item, $iparts ) ) ) {
30✔
939
                                $list[$k] = $v;
20✔
940
                        } else {
941
                                $list[$key] = $v;
12✔
942
                        }
943
                }
944

945
                return new static( $list );
30✔
946
        }
947

948

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

983
                $result = [];
50✔
984
                $this->kflatten( $this->list(), $result, $depth ?? 0x7fffffff );
50✔
985
                return new static( $result );
50✔
986
        }
987

988

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

1006

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

1022

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

1041
                foreach( $elements as $item ) {
20✔
1042
                        $this->list[] = $item;
20✔
1043
                }
1044

1045
                return $this;
20✔
1046
        }
1047

1048

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

1082
                if( $value === null ) {
10✔
1083
                        return !$this->where( $key, '==', $operator )->isEmpty();
10✔
1084
                }
1085

1086
                return !$this->where( $key, $operator, $value )->isEmpty();
10✔
1087
        }
1088

1089

1090
        /**
1091
         * Creates a new map with the same elements.
1092
         *
1093
         * Both maps share the same array until one of the map objects modifies the
1094
         * array. Then, the array is copied and the copy is modfied (copy on write).
1095
         *
1096
         * @return self<int|string,mixed> New map
1097
         */
1098
        public function copy() : self
1099
        {
1100
                return clone $this;
30✔
1101
        }
1102

1103

1104
        /**
1105
         * Counts the total number of elements in the map.
1106
         *
1107
         * @return int Number of elements
1108
         */
1109
        public function count() : int
1110
        {
1111
                return count( $this->list() );
170✔
1112
        }
1113

1114

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

1151
                        $col = function( $item ) use ( $parts ) {
24✔
1152
                                return (string) $this->val( $item, $parts );
30✔
1153
                        };
30✔
1154
                }
1155

1156
                return new static( array_count_values( array_map( $col, $this->list() ) ) );
40✔
1157
        }
1158

1159

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

1185

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

1221
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
30✔
1222
        }
1223

1224

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

1262
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
20✔
1263
        }
1264

1265

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

1302
                return new static( array_diff_key( $this->list(), $this->array( $elements ) ) );
20✔
1303
        }
1304

1305

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

1337

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

1370
                if( $col !== null ) {
40✔
1371
                        $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
30✔
1372
                }
1373

1374
                return new static( array_diff_key( $list, array_unique( $map ) ) );
40✔
1375
        }
1376

1377

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

1403
                return $this;
20✔
1404
        }
1405

1406

1407
        /**
1408
         * Determines if the map is empty or not.
1409
         *
1410
         * Examples:
1411
         *  Map::from( [] )->empty();
1412
         *  Map::from( ['a'] )->empty();
1413
         *
1414
         * Results:
1415
         *  The first example returns TRUE while the second returns FALSE
1416
         *
1417
         * The method is equivalent to isEmpty().
1418
         *
1419
         * @return bool TRUE if map is empty, FALSE if not
1420
         */
1421
        public function empty() : bool
1422
        {
1423
                return empty( $this->list() );
20✔
1424
        }
1425

1426

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

1452
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
60✔
1453
        }
1454

1455

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

1483
                return true;
10✔
1484
        }
1485

1486

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

1508

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

1538
                return new static( array_filter( $this->list() ) );
10✔
1539
        }
1540

1541

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

1572
                if( !empty( $list ) )
50✔
1573
                {
1574
                        if( $reverse )
50✔
1575
                        {
1576
                                $value = end( $list );
20✔
1577
                                $key = key( $list );
20✔
1578

1579
                                do
1580
                                {
1581
                                        if( $callback( $value, $key ) ) {
20✔
1582
                                                return $value;
10✔
1583
                                        }
1584
                                }
1585
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
20✔
1586

1587
                                reset( $list );
10✔
1588
                        }
1589
                        else
1590
                        {
1591
                                foreach( $list as $key => $value )
30✔
1592
                                {
1593
                                        if( $callback( $value, $key ) ) {
30✔
1594
                                                return $value;
12✔
1595
                                        }
1596
                                }
1597
                        }
1598
                }
1599

1600
                if( $default instanceof \Closure ) {
30✔
1601
                        return $default();
10✔
1602
                }
1603

1604
                if( $default instanceof \Throwable ) {
20✔
1605
                        throw $default;
10✔
1606
                }
1607

1608
                return $default;
10✔
1609
        }
1610

1611

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

1645
                if( !empty( $list ) )
50✔
1646
                {
1647
                        if( $reverse )
20✔
1648
                        {
1649
                                $value = end( $list );
10✔
1650
                                $key = key( $list );
10✔
1651

1652
                                do
1653
                                {
1654
                                        if( $callback( $value, $key ) ) {
10✔
1655
                                                return $key;
10✔
1656
                                        }
1657
                                }
1658
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
×
1659

1660
                                reset( $list );
×
1661
                        }
1662
                        else
1663
                        {
1664
                                foreach( $list as $key => $value )
10✔
1665
                                {
1666
                                        if( $callback( $value, $key ) ) {
10✔
1667
                                                return $key;
10✔
1668
                                        }
1669
                                }
1670
                        }
1671
                }
1672

1673
                if( $default instanceof \Closure ) {
30✔
1674
                        return $default();
10✔
1675
                }
1676

1677
                if( $default instanceof \Throwable ) {
20✔
1678
                        throw $default;
10✔
1679
                }
1680

1681
                return $default;
10✔
1682
        }
1683

1684

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

1711
                if( $default instanceof \Closure ) {
60✔
1712
                        return $default();
10✔
1713
                }
1714

1715
                if( $default instanceof \Throwable ) {
50✔
1716
                        throw $default;
20✔
1717
                }
1718

1719
                return $default;
30✔
1720
        }
1721

1722

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

1745
                // PHP 7.x compatibility
1746
                if( function_exists( 'array_key_first' ) ) {
50✔
1747
                        $key = array_key_first( $list );
50✔
1748
                } else {
1749
                        $key = key( array_slice( $list, 0, 1, true ) );
×
1750
                }
1751

1752
                if( $key !== null ) {
50✔
1753
                        return $key;
10✔
1754
                }
1755

1756
                if( $default instanceof \Closure ) {
40✔
1757
                        return $default();
10✔
1758
                }
1759

1760
                if( $default instanceof \Throwable ) {
30✔
1761
                        throw $default;
10✔
1762
                }
1763

1764
                return $default;
20✔
1765
        }
1766

1767

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

1801
                $result = [];
50✔
1802
                $this->nflatten( $this->list(), $result, $depth ?? 0x7fffffff );
50✔
1803
                return new static( $result );
50✔
1804
        }
1805

1806

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

1832
                $result = [];
10✔
1833
                $this->rflatten( $this->list(), $result, $depth ?? 0x7fffffff );
10✔
1834
                return new static( $result );
10✔
1835
        }
1836

1837

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

1854

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

1895

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

1925
                if( array_key_exists( $key, $list ) ) {
230✔
1926
                        return $list[$key];
60✔
1927
                }
1928

1929
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
210✔
1930
                        return $v;
70✔
1931
                }
1932

1933
                if( $default instanceof \Closure ) {
180✔
1934
                        return $default();
60✔
1935
                }
1936

1937
                if( $default instanceof \Throwable ) {
120✔
1938
                        throw $default;
60✔
1939
                }
1940

1941
                return $default;
60✔
1942
        }
1943

1944

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

1958

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

1997
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
10✔
1998
                }
1999

2000
                return new static( $result );
30✔
2001
        }
2002

2003

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

2055
                if( is_callable( $key ) )
40✔
2056
                {
2057
                        foreach( $this->list() as $idx => $item )
10✔
2058
                        {
2059
                                $keyval = (string) $key( $item, $idx );
10✔
2060
                                $result[$keyval][$idx] = $item;
10✔
2061
                        }
2062
                }
2063
                else
2064
                {
2065
                        $parts = explode( $this->sep, (string) $key );
30✔
2066

2067
                        foreach( $this->list() as $idx => $item )
30✔
2068
                        {
2069
                                $keyval = (string) $this->val( $item, $parts );
30✔
2070
                                $result[$keyval][$idx] = $item;
30✔
2071
                        }
2072
                }
2073

2074
                return new static( $result );
40✔
2075
        }
2076

2077

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

2107
                foreach( (array) $key as $entry )
30✔
2108
                {
2109
                        if( array_key_exists( $entry, $list ) === false
30✔
2110
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
30✔
2111
                        ) {
2112
                                return false;
30✔
2113
                        }
2114
                }
2115

2116
                return true;
30✔
2117
        }
2118

2119

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

2177
                if( $condition ) {
80✔
2178
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
50✔
2179
                } elseif( $else ) {
30✔
2180
                        return static::from( $else( $this, $condition ) );
30✔
2181
                }
2182

2183
                return $this;
×
2184
        }
2185

2186

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

2229

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

2267

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

2300
                                return false;
12✔
2301
                        }
2302
                }
2303

2304
                return true;
10✔
2305
        }
2306

2307

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

2331
                foreach( $element as $entry )
10✔
2332
                {
2333
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
10✔
2334
                                return false;
10✔
2335
                        }
2336
                }
2337

2338
                return true;
10✔
2339
        }
2340

2341

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

2358

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

2381
                        foreach( $this->list() as $key => $item )
20✔
2382
                        {
2383
                                if( $value( $key ) ) {
10✔
2384
                                        return $pos;
10✔
2385
                                }
2386

2387
                                ++$pos;
10✔
2388
                        }
2389

2390
                        return null;
10✔
2391
                }
2392

2393
                $pos = array_search( $value, array_keys( $this->list() ) );
20✔
2394
                return $pos !== false ? $pos : null;
20✔
2395
        }
2396

2397

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

2422
                return $this;
30✔
2423
        }
2424

2425

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

2452
                        $this->list = array_merge(
20✔
2453
                                array_slice( $list, 0, $pos, true ),
20✔
2454
                                [$key => $value],
20✔
2455
                                array_slice( $list, $pos, null, true )
20✔
2456
                        );
16✔
2457
                }
2458
                else
2459
                {
2460
                        array_splice( $this->list(), $pos, 0, [$value] );
30✔
2461
                }
2462

2463
                return $this;
50✔
2464
        }
2465

2466

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

2491
                return $this;
30✔
2492
        }
2493

2494

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

2530
                foreach( (array) $value as $val )
10✔
2531
                {
2532
                        if( (string) $val === '' ) {
10✔
2533
                                return true;
10✔
2534
                        }
2535

2536
                        foreach( $this->list() as $item )
10✔
2537
                        {
2538
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
10✔
2539
                                        return true;
10✔
2540
                                }
2541
                        }
2542
                }
2543

2544
                return false;
10✔
2545
        }
2546

2547

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

2588

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

2623
                if( $callback ) {
20✔
2624
                        return new static( array_uintersect( $list, $elements, $callback ) );
10✔
2625
                }
2626

2627
                return new static( array_intersect( $list, $elements ) );
10✔
2628
        }
2629

2630

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

2665
                if( $callback ) {
50✔
2666
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
10✔
2667
                }
2668

2669
                return new static( array_intersect_assoc( $this->list(), $elements ) );
40✔
2670
        }
2671

2672

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

2708
                if( $callback ) {
30✔
2709
                        return new static( array_intersect_ukey( $this->list(), $elements, $callback ) );
10✔
2710
                }
2711

2712
                return new static( array_intersect_key( $this->list(), $elements ) );
20✔
2713
        }
2714

2715

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

2735
                if( $strict ) {
30✔
2736
                        return $this->list() === $list;
20✔
2737
                }
2738

2739
                return $this->list() == $list;
10✔
2740
        }
2741

2742

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

2762

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

2783
                foreach( $this->list() as $k => $v )
10✔
2784
                {
2785
                        if( $k !== ++$i ) {
10✔
2786
                                return false;
10✔
2787
                        }
2788
                }
2789

2790
                return true;
10✔
2791
        }
2792

2793

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

2831
                return true;
10✔
2832
        }
2833

2834

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

2857
                return true;
10✔
2858
        }
2859

2860

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

2889
                return true;
10✔
2890
        }
2891

2892

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

2916

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

2945
                return true;
10✔
2946
        }
2947

2948

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

2971

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

2991

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

3010

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

3041

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

3071

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

3102

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

3132

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

3159
                if( $default instanceof \Closure ) {
40✔
3160
                        return $default();
10✔
3161
                }
3162

3163
                if( $default instanceof \Throwable ) {
30✔
3164
                        throw $default;
10✔
3165
                }
3166

3167
                return $default;
20✔
3168
        }
3169

3170

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

3193
                // PHP 7.x compatibility
3194
                if( function_exists( 'array_key_last' ) ) {
50✔
3195
                        $key = array_key_last( $list );
50✔
3196
                } else {
3197
                        $key = key( array_slice( $list, -1, 1, true ) );
×
3198
                }
3199

3200
                if( $key !== null ) {
50✔
3201
                        return $key;
10✔
3202
                }
3203

3204
                if( $default instanceof \Closure ) {
40✔
3205
                        return $default();
10✔
3206
                }
3207

3208
                if( $default instanceof \Throwable ) {
30✔
3209
                        throw $default;
10✔
3210
                }
3211

3212
                return $default;
20✔
3213
        }
3214

3215

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

3238
                return $this;
10✔
3239
        }
3240

3241

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

3266
                return new static( array_combine( $keys, $map ) );
10✔
3267
        }
3268

3269

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

3305
                return !empty( $vals ) ? max( $vals ) : null;
40✔
3306
        }
3307

3308

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

3343
                return $this;
30✔
3344
        }
3345

3346

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

3382
                return !empty( $vals ) ? min( $vals ) : null;
40✔
3383
        }
3384

3385

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

3408
                if( !is_array( $element ) ) {
10✔
3409
                        return !in_array( $element, $list, $strict );
10✔
3410
                };
3411

3412
                foreach( $element as $entry )
10✔
3413
                {
3414
                        if( in_array( $entry, $list, $strict ) === true ) {
10✔
3415
                                return false;
10✔
3416
                        }
3417
                }
3418

3419
                return true;
10✔
3420
        }
3421

3422

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

3444
                if( $step === 1 ) {
20✔
3445
                        return clone $this;
10✔
3446
                }
3447

3448
                $result = [];
10✔
3449
                $list = $this->list();
10✔
3450

3451
                while( !empty( $pair = array_slice( $list, $offset, 1, true ) ) )
10✔
3452
                {
3453
                        $result += $pair;
10✔
3454
                        $offset += $step;
10✔
3455
                }
3456

3457
                return new static( $result );
10✔
3458
        }
3459

3460

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

3481

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

3501

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

3525

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

3543

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

3565

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

3594
                foreach( $keys as $key ) {
10✔
3595
                        $result[$key] = $list[$key] ?? null;
10✔
3596
                }
3597

3598
                return new static( $result );
10✔
3599
        }
3600

3601

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

3638

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

3661
                if( empty( $list ) ) {
40✔
3662
                        return new static();
10✔
3663
                }
3664

3665
                $result = [];
30✔
3666

3667
                if( $number instanceof \Closure )
30✔
3668
                {
3669
                        foreach( $list as $idx => $item ) {
10✔
3670
                                $result[$number( $item, $idx )][$idx] = $item;
10✔
3671
                        }
3672

3673
                        return new static( $result );
10✔
3674
                }
3675

3676
                if( is_int( $number ) )
20✔
3677
                {
3678
                        $start = 0;
10✔
3679
                        $size = (int) ceil( count( $list ) / $number );
10✔
3680

3681
                        for( $i = 0; $i < $number; $i++ )
10✔
3682
                        {
3683
                                $result[] = array_slice( $list, $start, $size, true );
10✔
3684
                                $start += $size;
10✔
3685
                        }
3686

3687
                        return new static( $result );
10✔
3688
                }
3689

3690
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
10✔
3691
        }
3692

3693

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

3717
                $cnt = count( $this->list() );
10✔
3718
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
10✔
3719
        }
3720

3721

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

3741

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

3758

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

3775

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

3797
                if( $value instanceof \Closure )
150✔
3798
                {
3799
                        foreach( $list as $key => $item )
30✔
3800
                        {
3801
                                if( $value( $item, $key ) ) {
30✔
3802
                                        return $pos;
30✔
3803
                                }
3804

3805
                                ++$pos;
30✔
3806
                        }
3807

3808
                        return null;
×
3809
                }
3810

3811
                if( ( $key = array_search( $value, $list, true ) ) !== false
120✔
3812
                        && ( $pos = array_search( $key, array_keys( $list ), true ) ) !== false
120✔
3813
                ) {
3814
                        return $pos;
100✔
3815
                }
3816

3817
                return null;
20✔
3818
        }
3819

3820

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

3850
                        foreach( $list as $key => $item )
10✔
3851
                        {
3852
                                if( is_array( $item ) ) {
10✔
3853
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
10✔
3854
                                } else {
3855
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
10✔
3856
                                }
3857
                        }
3858

3859
                        return $list;
10✔
3860
                };
10✔
3861

3862
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
10✔
3863
                return $this;
10✔
3864
        }
3865

3866

3867
        /**
3868
         * Pushes an element onto the beginning of the map without returning a new map.
3869
         *
3870
         * This method is an alias for unshift().
3871
         *
3872
         * @param mixed $value Item to add at the beginning
3873
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3874
         * @return self<int|string,mixed> Updated map for fluid interface
3875
         * @see unshift() - Underlying method with same parameters and return value but better performance
3876
         */
3877
        public function prepend( $value, $key = null ) : self
3878
        {
3879
                return $this->unshift( $value, $key );
10✔
3880
        }
3881

3882

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

3908
                return $value;
30✔
3909
        }
3910

3911

3912
        /**
3913
         * Pushes an element onto the end of the map without returning a new map.
3914
         *
3915
         * Examples:
3916
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3917
         *
3918
         * Results:
3919
         *  ['a', 'b', 'aa']
3920
         *
3921
         * @param mixed $value Value to add to the end
3922
         * @return self<int|string,mixed> Updated map for fluid interface
3923
         */
3924
        public function push( $value ) : self
3925
        {
3926
                $this->list()[] = $value;
30✔
3927
                return $this;
30✔
3928
        }
3929

3930

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

3947

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

3975
                $list = $this->list();
40✔
3976

3977
                if( empty( $list ) ) {
40✔
3978
                        return new static();
10✔
3979
                }
3980

3981
                if( ( $num = count( $list ) ) < $max ) {
30✔
3982
                        $max = $num;
10✔
3983
                }
3984

3985
                $keys = array_rand( $list, $max );
30✔
3986

3987
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
30✔
3988
        }
3989

3990

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

4012

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

4041
                if( $callback instanceof \Closure )
30✔
4042
                {
4043
                        foreach( $this->list() as $key => $value )
10✔
4044
                        {
4045
                                if( !$callback( $value, $key ) ) {
10✔
4046
                                        $result[$key] = $value;
10✔
4047
                                }
4048
                        }
4049
                }
4050
                else
4051
                {
4052
                        foreach( $this->list() as $key => $value )
20✔
4053
                        {
4054
                                if( $value != $callback ) {
20✔
4055
                                        $result[$key] = $value;
20✔
4056
                                }
4057
                        }
4058
                }
4059

4060
                return new static( $result );
30✔
4061
        }
4062

4063

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

4085
                return new static( array_combine( $newKeys, array_values( $list ) ) );
10✔
4086
        }
4087

4088

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

4109
                return $this;
50✔
4110
        }
4111

4112

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

4141
                return $this;
50✔
4142
        }
4143

4144

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

4170
                if( !( $value instanceof \Closure ) )
100✔
4171
                {
4172
                        if( $key === null )
80✔
4173
                        {
4174
                                $filter = function( $v ) use ( $value ) {
56✔
4175
                                        return $v === $value;
70✔
4176
                                };
70✔
4177
                        }
4178
                        else
4179
                        {
4180
                                $filter = function( $v, $k ) use ( $key, $value ) {
8✔
4181
                                        return ( $v[$key] ?? null ) === $value;
10✔
4182
                                };
10✔
4183
                        }
4184
                }
4185

4186
                return $this->filter( $filter );
100✔
4187
        }
4188

4189

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

4212

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

4235

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

4266

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

4296

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

4319
                return $this;
10✔
4320
        }
4321

4322

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

4344
                return null;
10✔
4345
        }
4346

4347

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

4370

4371
        /**
4372
         * Sets an element in the map by key without returning a new map.
4373
         *
4374
         * Examples:
4375
         *  Map::from( ['a'] )->set( 1, 'b' );
4376
         *  Map::from( ['a'] )->set( 0, 'b' );
4377
         *
4378
         * Results:
4379
         *  ['a', 'b']
4380
         *  ['b']
4381
         *
4382
         * @param int|string $key Key to set the new value for
4383
         * @param mixed $value New element that should be set
4384
         * @return self<int|string,mixed> Updated map for fluid interface
4385
         */
4386
        public function set( $key, $value ) : self
4387
        {
4388
                $this->list()[(string) $key] = $value;
50✔
4389
                return $this;
50✔
4390
        }
4391

4392

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

4421

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

4447
                        foreach( $keys as $key ) {
10✔
4448
                                $items[$key] = $list[$key];
10✔
4449
                        }
4450

4451
                        $this->list = $items;
10✔
4452
                }
4453
                else
4454
                {
4455
                        shuffle( $this->list() );
20✔
4456
                }
4457

4458
                return $this;
30✔
4459
        }
4460

4461

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

4483

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

4508
                if( $offset instanceof \Closure ) {
20✔
4509
                        return new static( array_slice( $this->list(), $this->until( $this->list(), $offset ), null, true ) );
10✔
4510
                }
4511

4512
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as first argument for skip()' );
10✔
4513
        }
4514

4515

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

4550

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

4571
                for( $i = 0; $i < $chunks; $i++ ) {
20✔
4572
                        $result[] = array_slice( $this->list(), $i * $step, $size, true );
20✔
4573
                }
4574

4575
                return new static( $result );
20✔
4576
        }
4577

4578

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

4603
                if( $items->count() > 1 ) {
30✔
4604
                        throw new \LengthException( 'Multiple items found' );
10✔
4605
                }
4606

4607
                return $items->first( new \LengthException( 'No items found' ) );
20✔
4608
        }
4609

4610

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

4634
                if( is_iterable( $values ) )
60✔
4635
                {
4636
                        foreach( $values as $entry )
30✔
4637
                        {
4638
                                if( in_array( $entry, $list, $strict ) === true ) {
30✔
4639
                                        return true;
30✔
4640
                                }
4641
                        }
4642

4643
                        return false;
20✔
4644
                }
4645

4646
                if( $values instanceof \Closure )
40✔
4647
                {
4648
                        foreach( $list as $key => $item )
20✔
4649
                        {
4650
                                if( $values( $item, $key ) ) {
20✔
4651
                                        return true;
20✔
4652
                                }
4653
                        }
4654
                }
4655

4656
                return in_array( $values, $list, $strict );
40✔
4657
        }
4658

4659

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

4691

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

4723

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

4761
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
50✔
4762
        }
4763

4764

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

4802
                foreach( $this->list() as $key => $entry )
10✔
4803
                {
4804
                        if( is_scalar( $entry ) )
10✔
4805
                        {
4806
                                $pos = null;
10✔
4807
                                $str = (string) $entry;
10✔
4808

4809
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
10✔
4810
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
10✔
4811
                                } elseif( $str !== '' && $pos !== false ) {
10✔
4812
                                        $list[$key] = $str;
10✔
4813
                                }
4814
                        }
4815
                }
4816

4817
                return new static( $list );
10✔
4818
        }
4819

4820

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

4857
                foreach( $this->list() as $key => $entry )
10✔
4858
                {
4859
                        if( is_scalar( $entry ) )
10✔
4860
                        {
4861
                                $pos = null;
10✔
4862
                                $str = (string) $entry;
10✔
4863

4864
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
10✔
4865
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
10✔
4866
                                } elseif( $str !== '' && $pos !== false ) {
10✔
4867
                                        $list[$key] = $str;
10✔
4868
                                }
4869
                        }
4870
                }
4871

4872
                return new static( $list );
10✔
4873
        }
4874

4875

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

4902
                foreach( $this->list() as $item )
20✔
4903
                {
4904
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
20✔
4905
                                return true;
20✔
4906
                        }
4907
                }
4908

4909
                return false;
20✔
4910
        }
4911

4912

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

4950
                        foreach( (array) $value as $str )
10✔
4951
                        {
4952
                                $str = (string) $str;
10✔
4953

4954
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
10✔
4955
                                        return true;
10✔
4956
                                }
4957
                        }
4958
                }
4959

4960
                return false;
10✔
4961
        }
4962

4963

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

4997
                foreach( $this->list() as $entry )
10✔
4998
                {
4999
                        $entry = (string) $entry;
10✔
5000
                        $list[$entry] = 0;
10✔
5001

5002
                        foreach( (array) $value as $str )
10✔
5003
                        {
5004
                                $str = (string) $str;
10✔
5005

5006
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
10✔
5007
                                        $list[$entry] = 1; break;
10✔
5008
                                }
5009
                        }
5010
                }
5011

5012
                return array_sum( $list ) === count( $list );
10✔
5013
        }
5014

5015

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

5044
                        foreach( (array) $value as $str )
10✔
5045
                        {
5046
                                $len = mb_strlen( (string) $str );
10✔
5047

5048
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
10✔
5049
                                        return true;
10✔
5050
                                }
5051
                        }
5052
                }
5053

5054
                return false;
10✔
5055
        }
5056

5057

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

5084
                foreach( $this->list() as $entry )
10✔
5085
                {
5086
                        $entry = (string) $entry;
10✔
5087
                        $list[$entry] = 0;
10✔
5088

5089
                        foreach( (array) $value as $str )
10✔
5090
                        {
5091
                                $len = mb_strlen( (string) $str );
10✔
5092

5093
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
10✔
5094
                                        $list[$entry] = 1; break;
10✔
5095
                                }
5096
                        }
5097
                }
5098

5099
                return array_sum( $list ) === count( $list );
10✔
5100
        }
5101

5102

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

5141

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

5168
                return $this;
10✔
5169
        }
5170

5171

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

5218
                foreach( $this->list() as &$entry )
10✔
5219
                {
5220
                        if( is_string( $entry ) ) {
10✔
5221
                                $entry = $fcn( $search, $replace, $entry );
10✔
5222
                        }
5223
                }
5224

5225
                return $this;
10✔
5226
        }
5227

5228

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

5257
                        foreach( (array) $value as $str )
10✔
5258
                        {
5259
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
10✔
5260
                                        return true;
10✔
5261
                                }
5262
                        }
5263
                }
5264

5265
                return false;
10✔
5266
        }
5267

5268

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

5295
                foreach( $this->list() as $entry )
10✔
5296
                {
5297
                        $entry = (string) $entry;
10✔
5298
                        $list[$entry] = 0;
10✔
5299

5300
                        foreach( (array) $value as $str )
10✔
5301
                        {
5302
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
10✔
5303
                                        $list[$entry] = 1; break;
10✔
5304
                                }
5305
                        }
5306
                }
5307

5308
                return array_sum( $list ) === count( $list );
10✔
5309
        }
5310

5311

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

5338
                return $this;
10✔
5339
        }
5340

5341

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

5371
                        foreach( $list as $key => $item )
10✔
5372
                        {
5373
                                if( is_array( $item ) ) {
10✔
5374
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
10✔
5375
                                } else {
5376
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
10✔
5377
                                }
5378
                        }
5379

5380
                        return $list;
10✔
5381
                };
10✔
5382

5383
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
10✔
5384
                return $this;
10✔
5385
        }
5386

5387

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

5418
                return array_sum( $vals );
30✔
5419
        }
5420

5421

5422
        /**
5423
         * Returns a new map with the given number of items.
5424
         *
5425
         * The keys of the items returned in the new map are the same as in the original one.
5426
         *
5427
         * Examples:
5428
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
5429
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
5430
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
5431
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
5432
         *      return $item < 2;
5433
         *  } );
5434
         *
5435
         * Results:
5436
         *  [0 => 1, 1 => 2]
5437
         *  [1 => 2, 2 => 3]
5438
         *  [2 => 3, 3 => 4]
5439
         *  [1 => 2, 2 => 3]
5440
         *
5441
         * The keys of the items returned in the new map are the same as in the original one.
5442
         *
5443
         * @param int $size Number of items to return
5444
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5445
         * @return self<int|string,mixed> New map
5446
         */
5447
        public function take( int $size, $offset = 0 ) : self
5448
        {
5449
                $list = $this->list();
50✔
5450

5451
                if( is_numeric( $offset ) ) {
50✔
5452
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
30✔
5453
                }
5454

5455
                if( $offset instanceof \Closure ) {
20✔
5456
                        return new static( array_slice( $list, $this->until( $list, $offset ), $size, true ) );
10✔
5457
                }
5458

5459
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as second argument for take()' );
10✔
5460
        }
5461

5462

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

5489

5490
        /**
5491
         * Returns the elements as a plain array.
5492
         *
5493
         * @return array<int|string,mixed> Plain array
5494
         */
5495
        public function to() : array
5496
        {
5497
                return $this->list = $this->array( $this->list );
10✔
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 toArray() : array
5507
        {
5508
                return $this->list = $this->array( $this->list );
2,480✔
5509
        }
5510

5511

5512
        /**
5513
         * Returns the elements encoded as JSON string.
5514
         *
5515
         * There are several options available to modify the JSON output:
5516
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5517
         * The parameter can be a single JSON_* constant or a bitmask of several
5518
         * constants combine by bitwise OR (|), e.g.:
5519
         *
5520
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5521
         *
5522
         * @param int $options Combination of JSON_* constants
5523
         * @return string|null Array encoded as JSON string or NULL on failure
5524
         */
5525
        public function toJson( int $options = 0 ) : ?string
5526
        {
5527
                $result = json_encode( $this->list(), $options );
20✔
5528
                return $result !== false ? $result : null;
20✔
5529
        }
5530

5531

5532
        /**
5533
         * Reverses the element order in a copy of the map (alias).
5534
         *
5535
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5536
         * preferred because it uses one method call less than toReversed().
5537
         *
5538
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5539
         * @see reversed() - Underlying method with same parameters and return value but better performance
5540
         */
5541
        public function toReversed() : self
5542
        {
5543
                return $this->reversed();
10✔
5544
        }
5545

5546

5547
        /**
5548
         * Sorts the elements in a copy of the map using new keys (alias).
5549
         *
5550
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5551
         * preferred because it uses one method call less than toSorted().
5552
         *
5553
         * @param int $options Sort options for PHP sort()
5554
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5555
         * @see sorted() - Underlying method with same parameters and return value but better performance
5556
         */
5557
        public function toSorted( int $options = SORT_REGULAR ) : self
5558
        {
5559
                return $this->sorted( $options );
10✔
5560
        }
5561

5562

5563
        /**
5564
         * Creates a HTTP query string from the map elements.
5565
         *
5566
         * Examples:
5567
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5568
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5569
         *
5570
         * Results:
5571
         *  a=1&b=2
5572
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5573
         *
5574
         * @return string Parameter string for GET requests
5575
         */
5576
        public function toUrl() : string
5577
        {
5578
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
20✔
5579
        }
5580

5581

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

5616
                foreach( $this->list() as $key => $value )
40✔
5617
                {
5618
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
40✔
5619
                                $result[$newkey] = $newval;
40✔
5620
                        }
5621
                }
5622

5623
                return new static( $result );
40✔
5624
        }
5625

5626

5627
        /**
5628
         * Exchanges rows and columns for a two dimensional map.
5629
         *
5630
         * Examples:
5631
         *  Map::from( [
5632
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5633
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5634
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5635
         *  ] )->transpose();
5636
         *
5637
         *  Map::from( [
5638
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5639
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5640
         *    ['name' => 'C', 2020 => 400]
5641
         *  ] );
5642
         *
5643
         * Results:
5644
         *  [
5645
         *    'name' => ['A', 'B', 'C'],
5646
         *    2020 => [200, 300, 400],
5647
         *    2021 => [100, 200, 300],
5648
         *    2022 => [50, 100, 200]
5649
         *  ]
5650
         *
5651
         *  [
5652
         *    'name' => ['A', 'B', 'C'],
5653
         *    2020 => [200, 300, 400],
5654
         *    2021 => [100, 200],
5655
         *    2022 => [50]
5656
         *  ]
5657
         *
5658
         * @return self<int|string,mixed> New map
5659
         */
5660
        public function transpose() : self
5661
        {
5662
                $result = [];
20✔
5663

5664
                foreach( (array) $this->first( [] ) as $key => $col ) {
20✔
5665
                        $result[$key] = array_column( $this->list(), $key );
20✔
5666
                }
5667

5668
                return new static( $result );
20✔
5669
        }
5670

5671

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

5741
                return map( $result );
50✔
5742
        }
5743

5744

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

5802
                foreach( $this->list as &$node )
10✔
5803
                {
5804
                        $node[$nestKey] = [];
10✔
5805
                        $refs[$node[$idKey]] = &$node;
10✔
5806

5807
                        if( $node[$parentKey] ) {
10✔
5808
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
10✔
5809
                        } else {
5810
                                $trees[$node[$idKey]] = &$node;
10✔
5811
                        }
5812
                }
5813

5814
                return map( $trees );
10✔
5815
        }
5816

5817

5818
        /**
5819
         * Removes the passed characters from the left/right of all strings.
5820
         *
5821
         * Examples:
5822
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5823
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5824
         *
5825
         * Results:
5826
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5827
         *
5828
         * @param string $chars List of characters to trim
5829
         * @return self<int|string,mixed> Updated map for fluid interface
5830
         */
5831
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5832
        {
5833
                foreach( $this->list() as &$entry )
10✔
5834
                {
5835
                        if( is_string( $entry ) ) {
10✔
5836
                                $entry = trim( $entry, $chars );
10✔
5837
                        }
5838
                }
5839

5840
                return $this;
10✔
5841
        }
5842

5843

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

5873

5874
        /**
5875
         * Sorts all elements using a callback and maintains the key association.
5876
         *
5877
         * The given callback will be used to compare the values. The callback must accept
5878
         * two parameters (item A and B) and must return -1 if item A is smaller than
5879
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5880
         * method name and an anonymous function can be passed.
5881
         *
5882
         * Examples:
5883
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( 'strcasecmp' );
5884
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( function( $itemA, $itemB ) {
5885
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5886
         *  } );
5887
         *
5888
         * Results:
5889
         *  ['b' => 'a', 'a' => 'B']
5890
         *  ['b' => 'a', 'a' => 'B']
5891
         *
5892
         * The keys are preserved using this method and a new map is created.
5893
         *
5894
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5895
         * @return self<int|string,mixed> Updated map for fluid interface
5896
         */
5897
        public function uasorted( callable $callback ) : self
5898
        {
5899
                return ( clone $this )->uasort( $callback );
10✔
5900
        }
5901

5902

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

5932

5933
        /**
5934
         * Sorts a copy of the map elements by their keys using a callback.
5935
         *
5936
         * The given callback will be used to compare the keys. The callback must accept
5937
         * two parameters (key A and B) and must return -1 if key A is smaller than
5938
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5939
         * method name and an anonymous function can be passed.
5940
         *
5941
         * Examples:
5942
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5943
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5944
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5945
         *  } );
5946
         *
5947
         * Results:
5948
         *  ['a' => 'b', 'B' => 'a']
5949
         *  ['a' => 'b', 'B' => 'a']
5950
         *
5951
         * The keys are preserved using this method and a new map is created.
5952
         *
5953
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5954
         * @return self<int|string,mixed> Updated map for fluid interface
5955
         */
5956
        public function uksorted( callable $callback ) : self
5957
        {
5958
                return ( clone $this )->uksort( $callback );
10✔
5959
        }
5960

5961

5962
        /**
5963
         * Unflattens the key path/value pairs into a multi-dimensional array.
5964
         *
5965
         * Examples:
5966
         *  Map::from( ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3] )->unflatten();
5967
         *  Map::from( ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3] )->sep( '.' )->unflatten();
5968
         *
5969
         * Results:
5970
         * ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]]
5971
         *
5972
         * This is the inverse method for flatten().
5973
         *
5974
         * @return self<int|string,mixed> New map with multi-dimensional arrays
5975
         */
5976
        public function unflatten() : self
5977
        {
5978
                $result = [];
10✔
5979

5980
                foreach( $this->list() as $key => $value )
10✔
5981
                {
5982
                        $nested = &$result;
10✔
5983
                        $parts = explode( $this->sep, $key );
10✔
5984

5985
                        while( count( $parts ) > 1 ) {
10✔
5986
                                $nested = &$nested[array_shift( $parts )] ?? [];
10✔
5987
                        }
5988

5989
                        $nested[array_shift( $parts )] = $value;
10✔
5990
                }
5991

5992
                return new static( $result );
10✔
5993
        }
5994

5995

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

6021

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

6051
                $list = $this->list();
30✔
6052
                $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
30✔
6053

6054
                return new static( array_intersect_key( $list, array_unique( $map ) ) );
30✔
6055
        }
6056

6057

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

6093
                return $this;
30✔
6094
        }
6095

6096

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

6126

6127
        /**
6128
         * Sorts a copy of all elements using a callback using new keys.
6129
         *
6130
         * The given callback will be used to compare the values. The callback must accept
6131
         * two parameters (item A and B) and must return -1 if item A is smaller than
6132
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
6133
         * method name and an anonymous function can be passed.
6134
         *
6135
         * Examples:
6136
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
6137
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
6138
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
6139
         *  } );
6140
         *
6141
         * Results:
6142
         *  [0 => 'a', 1 => 'B']
6143
         *  [0 => 'a', 1 => 'B']
6144
         *
6145
         * The keys aren't preserved, elements get a new index and a new map is created.
6146
         *
6147
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
6148
         * @return self<int|string,mixed> Updated map for fluid interface
6149
         */
6150
        public function usorted( callable $callback ) : self
6151
        {
6152
                return ( clone $this )->usort( $callback );
10✔
6153
        }
6154

6155

6156
        /**
6157
         * Resets the keys and return the values in a new map.
6158
         *
6159
         * Examples:
6160
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
6161
         *
6162
         * Results:
6163
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
6164
         *
6165
         * @return self<int|string,mixed> New map of the values
6166
         */
6167
        public function values() : self
6168
        {
6169
                return new static( array_values( $this->list() ) );
100✔
6170
        }
6171

6172

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

6216
                return $this;
30✔
6217
        }
6218

6219

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

6284
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
60✔
6285
                        {
6286
                                switch( $op )
5✔
6287
                                {
6288
                                        case '-':
50✔
6289
                                                $list = (array) $value;
10✔
6290
                                                return $val >= current( $list ) && $val <= end( $list );
10✔
6291
                                        case 'in': return in_array( $val, (array) $value );
40✔
6292
                                        case '<': return $val < $value;
30✔
6293
                                        case '>': return $val > $value;
30✔
6294
                                        case '<=': return $val <= $value;
20✔
6295
                                        case '>=': return $val >= $value;
20✔
6296
                                        case '===': return $val === $value;
20✔
6297
                                        case '!==': return $val !== $value;
20✔
6298
                                        case '!=': return $val != $value;
20✔
6299
                                        default: return $val == $value;
20✔
6300
                                }
6301
                        }
6302

6303
                        return false;
10✔
6304
                } );
60✔
6305
        }
6306

6307

6308
        /**
6309
         * Returns a copy of the map with the element at the given index replaced with the given value.
6310
         *
6311
         * Examples:
6312
         *  $m = Map::from( ['a' => 1] );
6313
         *  $m->with( 2, 'b' );
6314
         *  $m->with( 'a', 2 );
6315
         *
6316
         * Results:
6317
         *  ['a' => 1, 2 => 'b']
6318
         *  ['a' => 2]
6319
         *
6320
         * The original map ($m) stays untouched!
6321
         * This method is a shortcut for calling the copy() and set() methods.
6322
         *
6323
         * @param int|string $key Array key to set or replace
6324
         * @param mixed $value New value for the given key
6325
         * @return self<int|string,mixed> New map
6326
         */
6327
        public function with( $key, $value ) : self
6328
        {
6329
                return ( clone $this )->set( $key, $value );
10✔
6330
        }
6331

6332

6333
        /**
6334
         * Merges the values of all arrays at the corresponding index.
6335
         *
6336
         * Examples:
6337
         *  $en = ['one', 'two', 'three'];
6338
         *  $es = ['uno', 'dos', 'tres'];
6339
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
6340
         *
6341
         * Results:
6342
         *  [
6343
         *    [1, 'one', 'uno'],
6344
         *    [2, 'two', 'dos'],
6345
         *    [3, 'three', 'tres'],
6346
         *  ]
6347
         *
6348
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
6349
         * @return self<int|string,mixed> New map of arrays
6350
         */
6351
        public function zip( ...$arrays ) : self
6352
        {
6353
                $args = array_map( function( $items ) {
8✔
6354
                        return $this->array( $items );
10✔
6355
                }, $arrays );
10✔
6356

6357
                return new static( array_map( null, $this->list(), ...$args ) );
10✔
6358
        }
6359

6360

6361
        /**
6362
         * Returns a plain array of the given elements.
6363
         *
6364
         * @param mixed $elements List of elements or single value
6365
         * @return array<int|string,mixed> Plain array
6366
         */
6367
        protected function array( $elements ) : array
6368
        {
6369
                if( is_array( $elements ) ) {
2,630✔
6370
                        return $elements;
2,530✔
6371
                }
6372

6373
                if( $elements instanceof \Closure ) {
390✔
6374
                        return (array) $elements();
×
6375
                }
6376

6377
                if( $elements instanceof \Aimeos\Map ) {
390✔
6378
                        return $elements->toArray();
230✔
6379
                }
6380

6381
                if( is_iterable( $elements ) ) {
170✔
6382
                        return iterator_to_array( $elements, true );
30✔
6383
                }
6384

6385
                return $elements !== null ? [$elements] : [];
140✔
6386
        }
6387

6388

6389
        /**
6390
         * Flattens a multi-dimensional array or map into a single level array.
6391
         *
6392
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6393
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
6394
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6395
         */
6396
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
6397
        {
6398
                foreach( $entries as $key => $entry )
50✔
6399
                {
6400
                        if( is_iterable( $entry ) && $depth > 0 ) {
50✔
6401
                                $this->kflatten( $entry, $result, $depth - 1 );
50✔
6402
                        } else {
6403
                                $result[$key] = $entry;
50✔
6404
                        }
6405
                }
6406
        }
10✔
6407

6408

6409
        /**
6410
         * Returns a reference to the array of elements
6411
         *
6412
         * @return array Reference to the array of elements
6413
         */
6414
        protected function &list() : array
6415
        {
6416
                if( !is_array( $this->list ) ) {
3,760✔
6417
                        $this->list = $this->array( $this->list );
×
6418
                }
6419

6420
                return $this->list;
3,760✔
6421
        }
6422

6423

6424
        /**
6425
         * Returns a closure that retrieves the value for the passed key
6426
         *
6427
         * @param \Closure|string|null $key Closure or key (e.g. "key1/key2/key3") to retrieve the value for
6428
         * @return \Closure Closure that retrieves the value for the passed key
6429
         */
6430
        protected function mapper( $key = null ) : \Closure
6431
        {
6432
                if( $key instanceof \Closure ) {
140✔
6433
                        return $key;
60✔
6434
                }
6435

6436
                $parts = $key ? explode( $this->sep, (string) $key ) : [];
80✔
6437

6438
                return function( $item ) use ( $parts ) {
64✔
6439
                        return $this->val( $item, $parts );
80✔
6440
                };
80✔
6441
        }
6442

6443

6444
        /**
6445
         * Flattens a multi-dimensional array or map into a single level array.
6446
         *
6447
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6448
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
6449
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6450
         */
6451
        protected function nflatten( iterable $entries, array &$result, int $depth ) : void
6452
        {
6453
                foreach( $entries as $entry )
50✔
6454
                {
6455
                        if( is_iterable( $entry ) && $depth > 0 ) {
50✔
6456
                                $this->nflatten( $entry, $result, $depth - 1 );
40✔
6457
                        } else {
6458
                                $result[] = $entry;
50✔
6459
                        }
6460
                }
6461
        }
10✔
6462

6463

6464
        /**
6465
         * Flattens a multi-dimensional array or map into an array with joined keys.
6466
         *
6467
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6468
         * @param array<int|string,mixed> $result Will contain joined key/value pairs from the multi-dimensional arrays afterwards
6469
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6470
         * @param string $path Path prefix of the current key
6471
         */
6472
        protected function rflatten( iterable $entries, array &$result, int $depth, string $path = '' ) : void
6473
        {
6474
                foreach( $entries as $key => $entry )
10✔
6475
                {
6476
                        if( is_iterable( $entry ) && $depth > 0 ) {
10✔
6477
                                $this->rflatten( $entry, $result, $depth - 1, $path . $key . $this->sep );
10✔
6478
                        } else {
6479
                                $result[$path . $key] = $entry;
10✔
6480
                        }
6481
                }
6482
        }
2✔
6483

6484

6485
        /**
6486
         * Returns the position of the first element that doesn't match the condition
6487
         *
6488
         * @param iterable<int|string,mixed> $list List of elements to check
6489
         * @param \Closure $callback Closure with ($item, $key) arguments to check the condition
6490
         * @return int Position of the first element that doesn't match the condition
6491
         */
6492
        protected function until( iterable $list, \Closure $callback ) : int
6493
        {
6494
                $idx = 0;
20✔
6495

6496
                foreach( $list as $key => $item )
20✔
6497
                {
6498
                        if( !$callback( $item, $key ) ) {
20✔
6499
                                break;
20✔
6500
                        }
6501

6502
                        ++$idx;
20✔
6503
                }
6504

6505
                return $idx;
20✔
6506
        }
6507

6508

6509
        /**
6510
         * Returns a configuration value from an array.
6511
         *
6512
         * @param array<mixed>|object $entry The array or object to look at
6513
         * @param array<string> $parts Path parts to look for inside the array or object
6514
         * @return mixed Found value or null if no value is available
6515
         */
6516
        protected function val( $entry, array $parts )
6517
        {
6518
                foreach( $parts as $part )
470✔
6519
                {
6520
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
450✔
6521
                                $entry = $entry[$part];
260✔
6522
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
270✔
6523
                                $entry = $entry->{$part};
20✔
6524
                        } else {
6525
                                return null;
270✔
6526
                        }
6527
                }
6528

6529
                return $entry;
290✔
6530
        }
6531

6532

6533
        /**
6534
         * Visits each entry, calls the callback and returns the items in the result argument
6535
         *
6536
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
6537
         * @param array<mixed> $result Numerically indexed list of all visited entries
6538
         * @param int $level Current depth of the nodes in the tree
6539
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
6540
         * @param string $nestKey Key to the children of each entry
6541
         * @param array<mixed>|object|null $parent Parent entry
6542
         */
6543
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, $parent = null ) : void
6544
        {
6545
                foreach( $entries as $key => $entry )
50✔
6546
                {
6547
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
50✔
6548

6549
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
50✔
6550
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
40✔
6551
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
10✔
6552
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
14✔
6553
                        }
6554
                }
6555
        }
10✔
6556
}
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