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

aimeos / map / 12687301011

09 Jan 2025 09:39AM UTC coverage: 97.294% (-0.2%) from 97.513%
12687301011

push

github

aimeos
Fixed documentation

755 of 776 relevant lines covered (97.29%)

49.37 hits per line

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

97.28
/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;
3,128✔
53
                $this->list = $elements;
3,128✔
54
        }
782✔
55

56

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

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

82

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

118
                $result = [];
16✔
119

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

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

130

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

141

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

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

168
                return $old;
8✔
169
        }
170

171

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

210
                $limit = $limit ?: 1;
32✔
211
                $parts = mb_str_split( $string );
32✔
212

213
                if( $limit < 1 ) {
32✔
214
                        return new static( array_slice( $parts, 0, $limit ) );
8✔
215
                }
216

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

222
                        return new static( $result );
8✔
223
                }
224

225
                return new static( $parts );
16✔
226
        }
227

228

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

252

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

277
                return new static( $elements );
1,456✔
278
        }
279

280

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

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

318

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

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

348

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

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

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

389

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

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

421

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

432

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

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

463
                return false;
8✔
464
        }
465

466

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

501

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

535

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

570

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

604

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

631

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

671
                $cnt = count( $vals );
24✔
672
                return $cnt > 0 ? array_sum( $vals ) / $cnt : 0;
24✔
673
        }
674

675

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

703

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

744

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

771
                foreach( $this->list() as $key => $item )
8✔
772
                {
773
                        if( is_object( $item ) ) {
8✔
774
                                $result[$key] = $item->{$name}( ...$params );
8✔
775
                        }
776
                }
777

778
                return new static( $result );
8✔
779
        }
780

781

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

822
                return $this;
8✔
823
        }
824

825

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

853
                return new static( array_chunk( $this->list(), $size, $preserve ) );
16✔
854
        }
855

856

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

868

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

889
                foreach( $this->list() as $key => $item ) {
8✔
890
                        $list[$key] = is_object( $item ) ? clone $item : $item;
8✔
891
                }
892

893
                return new static( $list );
8✔
894
        }
895

896

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

936
                if( count( $vparts ) === 1 && count( $iparts ) === 1 ) {
136✔
937
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
96✔
938
                }
939

940
                $list = [];
72✔
941

942
                foreach( $this->list() as $item )
72✔
943
                {
944
                        $v = $valuecol !== null ? $this->val( $item, $vparts ) : $item;
72✔
945

946
                        if( $indexcol !== null && ( $key = $this->val( $item, $iparts ) ) !== null ) {
72✔
947
                                $list[(string) $key] = $v;
24✔
948
                        } else {
949
                                $list[] = $v;
51✔
950
                        }
951
                }
952

953
                return new static( $list );
72✔
954
        }
955

956

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

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

996

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

1014

1015
        /**
1016
         * Compares the value against all map elements.
1017
         *
1018
         * Examples:
1019
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
1020
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
1021
         *  Map::from( [123, 12.3] )->compare( '12.3' );
1022
         *  Map::from( [false, true] )->compare( '1' );
1023
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
1024
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
1025
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
1026
         *
1027
         * Results:
1028
         * The first four examples return TRUE, the last three examples will return FALSE.
1029
         *
1030
         * All scalar values (bool, float, int and string) are casted to string values before
1031
         * comparing to the given value. Non-scalar values in the map are ignored.
1032
         *
1033
         * @param string $value Value to compare map elements to
1034
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
1035
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
1036
         */
1037
        public function compare( string $value, bool $case = true ) : bool
1038
        {
1039
                $fcn = $case ? 'strcmp' : 'strcasecmp';
8✔
1040

1041
                foreach( $this->list() as $item )
8✔
1042
                {
1043
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
8✔
1044
                                return true;
8✔
1045
                        }
1046
                }
1047

1048
                return false;
8✔
1049
        }
1050

1051

1052
        /**
1053
         * Pushs all of the given elements onto the map with new keys without creating a new map.
1054
         *
1055
         * Examples:
1056
         *  Map::from( ['foo'] )->concat( new Map( ['bar'] ));
1057
         *
1058
         * Results:
1059
         *  ['foo', 'bar']
1060
         *
1061
         * The keys of the passed elements are NOT preserved!
1062
         *
1063
         * @param iterable<int|string,mixed> $elements List of elements
1064
         * @return self<int|string,mixed> Updated map for fluid interface
1065
         */
1066
        public function concat( iterable $elements ) : self
1067
        {
1068
                $this->list();
16✔
1069

1070
                foreach( $elements as $item ) {
16✔
1071
                        $this->list[] = $item;
16✔
1072
                }
1073

1074
                return $this;
16✔
1075
        }
1076

1077

1078
        /**
1079
         * Determines if an item exists in the map.
1080
         *
1081
         * This method combines the power of the where() method with some() to check
1082
         * if the map contains at least one of the passed values or conditions.
1083
         *
1084
         * Examples:
1085
         *  Map::from( ['a', 'b'] )->contains( 'a' );
1086
         *  Map::from( ['a', 'b'] )->contains( ['a', 'c'] );
1087
         *  Map::from( ['a', 'b'] )->contains( function( $item, $key ) {
1088
         *    return $item === 'a'
1089
         *  } );
1090
         *  Map::from( [['type' => 'name']] )->contains( 'type', 'name' );
1091
         *  Map::from( [['type' => 'name']] )->contains( 'type', '==', 'name' );
1092
         *
1093
         * Results:
1094
         * All method calls will return TRUE because at least "a" is included in the
1095
         * map or there's a "type" key with a value "name" like in the last two
1096
         * examples.
1097
         *
1098
         * Check the where() method for available operators.
1099
         *
1100
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
1101
         * @param string|null $op Operator used for comparison
1102
         * @param mixed $value Value used for comparison
1103
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
1104
         */
1105
        public function contains( $key, ?string $operator = null, $value = null ) : bool
1106
        {
1107
                if( $operator === null ) {
16✔
1108
                        return $this->some( $key );
8✔
1109
                }
1110

1111
                if( $value === null ) {
8✔
1112
                        return !$this->where( $key, '==', $operator )->isEmpty();
8✔
1113
                }
1114

1115
                return !$this->where( $key, $operator, $value )->isEmpty();
8✔
1116
        }
1117

1118

1119
        /**
1120
         * Creates a new map with the same elements.
1121
         *
1122
         * Both maps share the same array until one of the map objects modifies the
1123
         * array. Then, the array is copied and the copy is modfied (copy on write).
1124
         *
1125
         * @return self<int|string,mixed> New map
1126
         */
1127
        public function copy() : self
1128
        {
1129
                return clone $this;
24✔
1130
        }
1131

1132

1133
        /**
1134
         * Counts the total number of elements in the map.
1135
         *
1136
         * @return int Number of elements
1137
         */
1138
        public function count() : int
1139
        {
1140
                return count( $this->list() );
72✔
1141
        }
1142

1143

1144
        /**
1145
         * Counts how often the same values are in the map.
1146
         *
1147
         * Examples:
1148
         *  Map::from( [1, 'foo', 2, 'foo', 1] )->countBy();
1149
         *  Map::from( [1.11, 3.33, 3.33, 9.99] )->countBy();
1150
         *  Map::from( ['a@gmail.com', 'b@yahoo.com', 'c@gmail.com'] )->countBy( function( $email ) {
1151
         *    return substr( strrchr( $email, '@' ), 1 );
1152
         *  } );
1153
         *
1154
         * Results:
1155
         *  [1 => 2, 'foo' => 2, 2 => 1]
1156
         *  ['1.11' => 1, '3.33' => 2, '9.99' => 1]
1157
         *  ['gmail.com' => 2, 'yahoo.com' => 1]
1158
         *
1159
         * Counting values does only work for integers and strings because these are
1160
         * the only types allowed as array keys. All elements are casted to strings
1161
         * if no callback is passed. Custom callbacks need to make sure that only
1162
         * string or integer values are returned!
1163
         *
1164
         * @param  callable|null $callback Function with (value, key) parameters which returns the value to use for counting
1165
         * @return self<int|string,mixed> New map with values as keys and their count as value
1166
         */
1167
        public function countBy( ?callable $callback = null ) : self
1168
        {
1169
                $callback = $callback ?: function( $value ) {
18✔
1170
                        return (string) $value;
16✔
1171
                };
24✔
1172

1173
                return new static( array_count_values( array_map( $callback, $this->list() ) ) );
24✔
1174
        }
1175

1176

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

1202

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

1238
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
24✔
1239
        }
1240

1241

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

1279
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
16✔
1280
        }
1281

1282

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

1319
                return new static( array_diff_key( $this->list(), $this->array( $elements ) ) );
16✔
1320
        }
1321

1322

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

1354

1355
        /**
1356
         * Returns the duplicate values from the map.
1357
         *
1358
         * For nested arrays, you have to pass the name of the column of the nested
1359
         * array which should be used to check for duplicates.
1360
         *
1361
         * Examples:
1362
         *  Map::from( [1, 2, '1', 3] )->duplicates()
1363
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->duplicates( 'p' )
1364
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( 'i/p' )
1365
         *
1366
         * Results:
1367
         *  [2 => '1']
1368
         *  [1 => ['p' => 1]]
1369
         *  [1 => ['i' => ['p' => '1']]]
1370
         *
1371
         * This does also work for multi-dimensional arrays by passing the keys
1372
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1373
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1374
         * public properties of objects or objects implementing __isset() and __get() methods.
1375
         *
1376
         * The keys are preserved using this method.
1377
         *
1378
         * @param string|null $key Key or path of the nested array or object to check for
1379
         * @return self<int|string,mixed> New map
1380
         */
1381
        public function duplicates( ?string $key = null ) : self
1382
        {
1383
                $list = $this->list();
24✔
1384
                $items = ( $key !== null ? $this->col( $key )->toArray() : $list );
24✔
1385

1386
                return new static( array_diff_key( $list, array_unique( $items ) ) );
24✔
1387
        }
1388

1389

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

1415
                return $this;
16✔
1416
        }
1417

1418

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

1438

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

1464
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
48✔
1465
        }
1466

1467

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

1495
                return true;
8✔
1496
        }
1497

1498

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

1520

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

1549
                return new static( array_filter( $this->list() ) );
8✔
1550
        }
1551

1552

1553
        /**
1554
         * Returns the first/last matching element where the callback returns TRUE.
1555
         *
1556
         * Examples:
1557
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1558
         *      return $value >= 'b';
1559
         *  } );
1560
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1561
         *      return $value >= 'b';
1562
         *  }, null, true );
1563
         *  Map::from( [] )->find( function( $value, $key ) {
1564
         *      return $value >= 'b';
1565
         *  }, 'none' );
1566
         *  Map::from( [] )->find( function( $value, $key ) {
1567
         *      return $value >= 'b';
1568
         *  }, new \Exception( 'error' ) );
1569
         *
1570
         * Results:
1571
         * The first example will return 'c' while the second will return 'e' (last element).
1572
         * The third one will return "none" and the last one will throw the exception.
1573
         *
1574
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1575
         * @param mixed $default Default value or exception if the map contains no elements
1576
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1577
         * @return mixed First matching value, passed default value or an exception
1578
         */
1579
        public function find( \Closure $callback, $default = null, bool $reverse = false )
1580
        {
1581
                foreach( ( $reverse ? array_reverse( $this->list(), true ) : $this->list() ) as $key => $value )
32✔
1582
                {
1583
                        if( $callback( $value, $key ) ) {
32✔
1584
                                return $value;
18✔
1585
                        }
1586
                }
1587

1588
                if( $default instanceof \Throwable ) {
16✔
1589
                        throw $default;
8✔
1590
                }
1591

1592
                return $default;
8✔
1593
        }
1594

1595

1596
        /**
1597
         * Returns the first/last key where the callback returns TRUE.
1598
         *
1599
         * Examples:
1600
         *  Map::from( ['a', 'c', 'e'] )->findKey( function( $value, $key ) {
1601
         *      return $value >= 'b';
1602
         *  } );
1603
         *  Map::from( ['a', 'c', 'e'] )->findKey( function( $value, $key ) {
1604
         *      return $value >= 'b';
1605
         *  }, null, true );
1606
         *  Map::from( [] )->findKey( function( $value, $key ) {
1607
         *      return $value >= 'b';
1608
         *  }, 'none' );
1609
         *  Map::from( [] )->findKey( function( $value, $key ) {
1610
         *      return $value >= 'b';
1611
         *  }, new \Exception( 'error' ) );
1612
         *
1613
         * Results:
1614
         * The first example will return '1' while the second will return '2' (last element).
1615
         * The third one will return "none" and the last one will throw the exception.
1616
         *
1617
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1618
         * @param mixed $default Default value or exception if the map contains no elements
1619
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1620
         * @return mixed Key of first matching element, passed default value or an exception
1621
         */
1622
        public function findKey( \Closure $callback, $default = null, bool $reverse = false )
1623
        {
1624
                foreach( ( $reverse ? array_reverse( $this->list(), true ) : $this->list() ) as $key => $value )
32✔
1625
                {
1626
                        if( $callback( $value, $key ) ) {
16✔
1627
                                return $key;
16✔
1628
                        }
1629
                }
1630

1631
                if( $default instanceof \Throwable ) {
16✔
1632
                        throw $default;
8✔
1633
                }
1634

1635
                return $default;
8✔
1636
        }
1637

1638

1639
        /**
1640
         * Returns the first element from the map.
1641
         *
1642
         * Examples:
1643
         *  Map::from( ['a', 'b'] )->first();
1644
         *  Map::from( [] )->first( 'x' );
1645
         *  Map::from( [] )->first( new \Exception( 'error' ) );
1646
         *  Map::from( [] )->first( function() { return rand(); } );
1647
         *
1648
         * Results:
1649
         * The first example will return 'b' and the second one 'x'. The third example
1650
         * will throw the exception passed if the map contains no elements. In the
1651
         * fourth example, a random value generated by the closure function will be
1652
         * returned.
1653
         *
1654
         * @param mixed $default Default value or exception if the map contains no elements
1655
         * @return mixed First value of map, (generated) default value or an exception
1656
         */
1657
        public function first( $default = null )
1658
        {
1659
                if( ( $value = reset( $this->list() ) ) !== false ) {
64✔
1660
                        return $value;
40✔
1661
                }
1662

1663
                if( $default instanceof \Closure ) {
32✔
1664
                        return $default();
8✔
1665
                }
1666

1667
                if( $default instanceof \Throwable ) {
24✔
1668
                        throw $default;
8✔
1669
                }
1670

1671
                return $default;
16✔
1672
        }
1673

1674

1675
        /**
1676
         * Returns the first key from the map.
1677
         *
1678
         * Examples:
1679
         *  Map::from( ['a' => 1, 'b' => 2] )->firstKey();
1680
         *  Map::from( [] )->firstKey();
1681
         *
1682
         * Results:
1683
         * The first example will return 'a' and the second one NULL.
1684
         *
1685
         * @return mixed First key of map or NULL if empty
1686
         */
1687
        public function firstKey()
1688
        {
1689
                $list = $this->list();
16✔
1690

1691
                if( function_exists( 'array_key_first' ) ) {
16✔
1692
                        return array_key_first( $list );
16✔
1693
                }
1694

1695
                reset( $list );
×
1696
                return key( $list );
×
1697
        }
1698

1699

1700
        /**
1701
         * Creates a new map with all sub-array elements added recursively withput overwriting existing keys.
1702
         *
1703
         * Examples:
1704
         *  Map::from( [[0, 1], [2, 3]] )->flat();
1705
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat();
1706
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat( 1 );
1707
         *  Map::from( [[0, 1], Map::from( [[2, 3], 4] )] )->flat();
1708
         *
1709
         * Results:
1710
         *  [0, 1, 2, 3]
1711
         *  [0, 1, 2, 3, 4]
1712
         *  [0, 1, [2, 3], 4]
1713
         *  [0, 1, 2, 3, 4]
1714
         *
1715
         * The keys are not preserved and the new map elements will be numbered from
1716
         * 0-n. A value smaller than 1 for depth will return the same map elements
1717
         * indexed from 0-n. Flattening does also work if elements implement the
1718
         * "Traversable" interface (which the Map object does).
1719
         *
1720
         * This method is similar than collapse() but doesn't replace existing elements.
1721
         * Keys are NOT preserved using this method!
1722
         *
1723
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1724
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
1725
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1726
         */
1727
        public function flat( ?int $depth = null ) : self
1728
        {
1729
                if( $depth < 0 ) {
48✔
1730
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
8✔
1731
                }
1732

1733
                $result = [];
40✔
1734
                $this->flatten( $this->list(), $result, $depth ?? 0x7fffffff );
40✔
1735
                return new static( $result );
40✔
1736
        }
1737

1738

1739
        /**
1740
         * Exchanges the keys with their values and vice versa.
1741
         *
1742
         * Examples:
1743
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->flip();
1744
         *
1745
         * Results:
1746
         *  ['X' => 'a', 'Y' => 'b']
1747
         *
1748
         * @return self<int|string,mixed> New map with keys as values and values as keys
1749
         */
1750
        public function flip() : self
1751
        {
1752
                return new static( array_flip( $this->list() ) );
8✔
1753
        }
1754

1755

1756
        /**
1757
         * Returns an element by key and casts it to float if possible.
1758
         *
1759
         * Examples:
1760
         *  Map::from( ['a' => true] )->float( 'a' );
1761
         *  Map::from( ['a' => 1] )->float( 'a' );
1762
         *  Map::from( ['a' => '1.1'] )->float( 'a' );
1763
         *  Map::from( ['a' => '10'] )->float( 'a' );
1764
         *  Map::from( ['a' => ['b' => ['c' => 1.1]]] )->float( 'a/b/c' );
1765
         *  Map::from( [] )->float( 'c', function() { return 1.1; } );
1766
         *  Map::from( [] )->float( 'a', 1.1 );
1767
         *
1768
         *  Map::from( [] )->float( 'b' );
1769
         *  Map::from( ['b' => ''] )->float( 'b' );
1770
         *  Map::from( ['b' => null] )->float( 'b' );
1771
         *  Map::from( ['b' => 'abc'] )->float( 'b' );
1772
         *  Map::from( ['b' => [1]] )->float( 'b' );
1773
         *  Map::from( ['b' => #resource] )->float( 'b' );
1774
         *  Map::from( ['b' => new \stdClass] )->float( 'b' );
1775
         *
1776
         *  Map::from( [] )->float( 'c', new \Exception( 'error' ) );
1777
         *
1778
         * Results:
1779
         * The first eight examples will return the float values for the passed keys
1780
         * while the 9th to 14th example returns 0. The last example will throw an exception.
1781
         *
1782
         * This does also work for multi-dimensional arrays by passing the keys
1783
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1784
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1785
         * public properties of objects or objects implementing __isset() and __get() methods.
1786
         *
1787
         * @param int|string $key Key or path to the requested item
1788
         * @param mixed $default Default value if key isn't found (will be casted to float)
1789
         * @return float Value from map or default value
1790
         */
1791
        public function float( $key, $default = 0.0 ) : float
1792
        {
1793
                return (float) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
1794
        }
1795

1796

1797
        /**
1798
         * Returns an element from the map by key.
1799
         *
1800
         * Examples:
1801
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'a' );
1802
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'c', 'Z' );
1803
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->get( 'a/b/c' );
1804
         *  Map::from( [] )->get( 'Y', new \Exception( 'error' ) );
1805
         *  Map::from( [] )->get( 'Y', function() { return rand(); } );
1806
         *
1807
         * Results:
1808
         * The first example will return 'X', the second 'Z' and the third 'Y'. The forth
1809
         * example will throw the exception passed if the map contains no elements. In
1810
         * the fifth example, a random value generated by the closure function will be
1811
         * returned.
1812
         *
1813
         * This does also work for multi-dimensional arrays by passing the keys
1814
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1815
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1816
         * public properties of objects or objects implementing __isset() and __get() methods.
1817
         *
1818
         * @param int|string $key Key or path to the requested item
1819
         * @param mixed $default Default value if no element matches
1820
         * @return mixed Value from map or default value
1821
         */
1822
        public function get( $key, $default = null )
1823
        {
1824
                $list = $this->list();
184✔
1825

1826
                if( array_key_exists( $key, $list ) ) {
184✔
1827
                        return $list[$key];
48✔
1828
                }
1829

1830
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
168✔
1831
                        return $v;
56✔
1832
                }
1833

1834
                if( $default instanceof \Closure ) {
144✔
1835
                        return $default();
48✔
1836
                }
1837

1838
                if( $default instanceof \Throwable ) {
96✔
1839
                        throw $default;
48✔
1840
                }
1841

1842
                return $default;
48✔
1843
        }
1844

1845

1846
        /**
1847
         * Returns an iterator for the elements.
1848
         *
1849
         * This method will be used by e.g. foreach() to loop over all entries:
1850
         *  foreach( Map::from( ['a', 'b'] ) as $value )
1851
         *
1852
         * @return \ArrayIterator<int|string,mixed> Iterator for map elements
1853
         */
1854
        public function getIterator() : \ArrayIterator
1855
        {
1856
                return new \ArrayIterator( $this->list() );
40✔
1857
        }
1858

1859

1860
        /**
1861
         * Returns only items which matches the regular expression.
1862
         *
1863
         * All items are converted to string first before they are compared to the
1864
         * regular expression. Thus, fractions of ".0" will be removed in float numbers
1865
         * which may result in unexpected results.
1866
         *
1867
         * Examples:
1868
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/b/' );
1869
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/a/', PREG_GREP_INVERT );
1870
         *  Map::from( [1.5, 0, 1.0, 'a'] )->grep( '/^(\d+)?\.\d+$/' );
1871
         *
1872
         * Results:
1873
         *  ['ab', 'bc']
1874
         *  ['bc', 'cd']
1875
         *  [1.5] // float 1.0 is converted to string "1"
1876
         *
1877
         * The keys are preserved using this method.
1878
         *
1879
         * @param string $pattern Regular expression pattern, e.g. "/ab/"
1880
         * @param int $flags PREG_GREP_INVERT to return elements not matching the pattern
1881
         * @return self<int|string,mixed> New map containing only the matched elements
1882
         */
1883
        public function grep( string $pattern, int $flags = 0 ) : self
1884
        {
1885
                if( ( $result = preg_grep( $pattern, $this->list(), $flags ) ) === false )
32✔
1886
                {
1887
                        switch( preg_last_error() )
8✔
1888
                        {
1889
                                case PREG_INTERNAL_ERROR: $msg = 'Internal error'; break;
8✔
1890
                                case PREG_BACKTRACK_LIMIT_ERROR: $msg = 'Backtrack limit error'; break;
×
1891
                                case PREG_RECURSION_LIMIT_ERROR: $msg = 'Recursion limit error'; break;
×
1892
                                case PREG_BAD_UTF8_ERROR: $msg = 'Bad UTF8 error'; break;
×
1893
                                case PREG_BAD_UTF8_OFFSET_ERROR: $msg = 'Bad UTF8 offset error'; break;
×
1894
                                case PREG_JIT_STACKLIMIT_ERROR: $msg = 'JIT stack limit error'; break;
×
1895
                                default: $msg = 'Unknown error';
×
1896
                        }
1897

1898
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
8✔
1899
                }
1900

1901
                return new static( $result );
24✔
1902
        }
1903

1904

1905
        /**
1906
         * Groups associative array elements or objects by the passed key or closure.
1907
         *
1908
         * Instead of overwriting items with the same keys like to the col() method
1909
         * does, groupBy() keeps all entries in sub-arrays. It's preserves the keys
1910
         * of the orignal map entries too.
1911
         *
1912
         * Examples:
1913
         *  $list = [
1914
         *    10 => ['aid' => 123, 'code' => 'x-abc'],
1915
         *    20 => ['aid' => 123, 'code' => 'x-def'],
1916
         *    30 => ['aid' => 456, 'code' => 'x-def']
1917
         *  ];
1918
         *  Map::from( $list )->groupBy( 'aid' );
1919
         *  Map::from( $list )->groupBy( function( $item, $key ) {
1920
         *    return substr( $item['code'], -3 );
1921
         *  } );
1922
         *  Map::from( $list )->groupBy( 'xid' );
1923
         *
1924
         * Results:
1925
         *  [
1926
         *    123 => [10 => ['aid' => 123, 'code' => 'x-abc'], 20 => ['aid' => 123, 'code' => 'x-def']],
1927
         *    456 => [30 => ['aid' => 456, 'code' => 'x-def']]
1928
         *  ]
1929
         *  [
1930
         *    'abc' => [10 => ['aid' => 123, 'code' => 'x-abc']],
1931
         *    'def' => [20 => ['aid' => 123, 'code' => 'x-def'], 30 => ['aid' => 456, 'code' => 'x-def']]
1932
         *  ]
1933
         *  [
1934
         *    '' => [
1935
         *      10 => ['aid' => 123, 'code' => 'x-abc'],
1936
         *      20 => ['aid' => 123, 'code' => 'x-def'],
1937
         *      30 => ['aid' => 456, 'code' => 'x-def']
1938
         *    ]
1939
         *  ]
1940
         *
1941
         * In case the passed key doesn't exist in one or more items, these items
1942
         * are stored in a sub-array using an empty string as key.
1943
         *
1944
         * @param  \Closure|string|int $key Closure function with (item, idx) parameters returning the key or the key itself to group by
1945
         * @return self<int|string,mixed> New map with elements grouped by the given key
1946
         */
1947
        public function groupBy( $key ) : self
1948
        {
1949
                $result = [];
32✔
1950

1951
                foreach( $this->list() as $idx => $item )
32✔
1952
                {
1953
                        if( is_callable( $key ) ) {
32✔
1954
                                $keyval = $key( $item, $idx );
8✔
1955
                        } elseif( ( is_array( $item ) || $item instanceof \ArrayAccess ) && isset( $item[$key] ) ) {
24✔
1956
                                $keyval = $item[$key];
8✔
1957
                        } elseif( is_object( $item ) && isset( $item->{$key} ) ) {
16✔
1958
                                $keyval = $item->{$key};
8✔
1959
                        } else {
1960
                                $keyval = '';
8✔
1961
                        }
1962

1963
                        $result[$keyval][$idx] = $item;
32✔
1964
                }
1965

1966
                return new static( $result );
32✔
1967
        }
1968

1969

1970
        /**
1971
         * Determines if a key or several keys exists in the map.
1972
         *
1973
         * If several keys are passed as array, all keys must exist in the map for
1974
         * TRUE to be returned.
1975
         *
1976
         * Examples:
1977
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'a' );
1978
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'b'] );
1979
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->has( 'a/b/c' );
1980
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'c' );
1981
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'c'] );
1982
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'X' );
1983
         *
1984
         * Results:
1985
         * The first three examples will return TRUE while the other ones will return FALSE
1986
         *
1987
         * This does also work for multi-dimensional arrays by passing the keys
1988
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1989
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1990
         * public properties of objects or objects implementing __isset() and __get() methods.
1991
         *
1992
         * @param array<int|string>|int|string $key Key of the requested item or list of keys
1993
         * @return bool TRUE if key or keys are available in map, FALSE if not
1994
         */
1995
        public function has( $key ) : bool
1996
        {
1997
                $list = $this->list();
24✔
1998

1999
                foreach( (array) $key as $entry )
24✔
2000
                {
2001
                        if( array_key_exists( $entry, $list ) === false
24✔
2002
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
24✔
2003
                        ) {
2004
                                return false;
24✔
2005
                        }
2006
                }
2007

2008
                return true;
24✔
2009
        }
2010

2011

2012
        /**
2013
         * Executes callbacks depending on the condition.
2014
         *
2015
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2016
         * executed and their returned value is passed back within a Map object. In
2017
         * case no "then" or "else" closure is given, the method will return the same
2018
         * map object.
2019
         *
2020
         * Examples:
2021
         *  Map::from( [] )->if( strpos( 'abc', 'b' ) !== false, function( $map ) {
2022
         *    echo 'found';
2023
         *  } );
2024
         *
2025
         *  Map::from( [] )->if( function( $map ) {
2026
         *    return $map->empty();
2027
         *  }, function( $map ) {
2028
         *    echo 'then';
2029
         *  } );
2030
         *
2031
         *  Map::from( ['a'] )->if( function( $map ) {
2032
         *    return $map->empty();
2033
         *  }, function( $map ) {
2034
         *    echo 'then';
2035
         *  }, function( $map ) {
2036
         *    echo 'else';
2037
         *  } );
2038
         *
2039
         *  Map::from( ['a', 'b'] )->if( true, function( $map ) {
2040
         *    return $map->push( 'c' );
2041
         *  } );
2042
         *
2043
         *  Map::from( ['a', 'b'] )->if( false, null, function( $map ) {
2044
         *    return $map->pop();
2045
         *  } );
2046
         *
2047
         * Results:
2048
         * The first example returns "found" while the second one returns "then" and
2049
         * the third one "else". The forth one will return ['a', 'b', 'c'] while the
2050
         * fifth one will return 'b', which is turned into a map of ['b'] again.
2051
         *
2052
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2053
         * (a short form for anonymous closures) as parameters. The automatically have access
2054
         * to previously defined variables but can not modify them. Also, they can not have
2055
         * a void return type and must/will always return something. Details about
2056
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2057
         *
2058
         * @param \Closure|bool $condition Boolean or function with (map) parameter returning a boolean
2059
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2060
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2061
         * @return self<int|string,mixed> New map
2062
         */
2063
        public function if( $condition, ?\Closure $then = null, ?\Closure $else = null ) : self
2064
        {
2065
                if( $condition instanceof \Closure ) {
64✔
2066
                        $condition = $condition( $this );
16✔
2067
                }
2068

2069
                if( $condition ) {
64✔
2070
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
40✔
2071
                } elseif( $else ) {
24✔
2072
                        return static::from( $else( $this, $condition ) );
24✔
2073
                }
2074

2075
                return $this;
×
2076
        }
2077

2078

2079
        /**
2080
         * Executes callbacks depending if the map contains elements or not.
2081
         *
2082
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2083
         * executed and their returned value is passed back within a Map object. In
2084
         * case no "then" or "else" closure is given, the method will return the same
2085
         * map object.
2086
         *
2087
         * Examples:
2088
         *  Map::from( ['a'] )->ifAny( function( $map ) {
2089
         *    $map->push( 'b' );
2090
         *  } );
2091
         *
2092
         *  Map::from( [] )->ifAny( null, function( $map ) {
2093
         *    return $map->push( 'b' );
2094
         *  } );
2095
         *
2096
         *  Map::from( ['a'] )->ifAny( function( $map ) {
2097
         *    return 'c';
2098
         *  } );
2099
         *
2100
         * Results:
2101
         * The first example returns a Map containing ['a', 'b'] because the the initial
2102
         * Map is not empty. The second one returns  a Map with ['b'] because the initial
2103
         * Map is empty and the "else" closure is used. The last example returns ['c']
2104
         * as new map content.
2105
         *
2106
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2107
         * (a short form for anonymous closures) as parameters. The automatically have access
2108
         * to previously defined variables but can not modify them. Also, they can not have
2109
         * a void return type and must/will always return something. Details about
2110
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2111
         *
2112
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2113
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2114
         * @return self<int|string,mixed> New map
2115
         */
2116
        public function ifAny( ?\Closure $then = null, ?\Closure $else = null ) : self
2117
        {
2118
                return $this->if( !empty( $this->list() ), $then, $else );
24✔
2119
        }
2120

2121

2122
        /**
2123
         * Executes callbacks depending if the map is empty or not.
2124
         *
2125
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2126
         * executed and their returned value is passed back within a Map object. In
2127
         * case no "then" or "else" closure is given, the method will return the same
2128
         * map object.
2129
         *
2130
         * Examples:
2131
         *  Map::from( [] )->ifEmpty( function( $map ) {
2132
         *    $map->push( 'a' );
2133
         *  } );
2134
         *
2135
         *  Map::from( ['a'] )->ifEmpty( null, function( $map ) {
2136
         *    return $map->push( 'b' );
2137
         *  } );
2138
         *
2139
         * Results:
2140
         * The first example returns a Map containing ['a'] because the the initial Map
2141
         * is empty. The second one returns  a Map with ['a', 'b'] because the initial
2142
         * Map is not empty and the "else" closure is used.
2143
         *
2144
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2145
         * (a short form for anonymous closures) as parameters. The automatically have access
2146
         * to previously defined variables but can not modify them. Also, they can not have
2147
         * a void return type and must/will always return something. Details about
2148
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2149
         *
2150
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2151
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2152
         * @return self<int|string,mixed> New map
2153
         */
2154
        public function ifEmpty( ?\Closure $then = null, ?\Closure $else = null ) : self
2155
        {
2156
                return $this->if( empty( $this->list() ), $then, $else );
×
2157
        }
2158

2159

2160
        /**
2161
         * Tests if all entries in the map are objects implementing the given interface.
2162
         *
2163
         * Examples:
2164
         *  Map::from( [new Map(), new Map()] )->implements( '\Countable' );
2165
         *  Map::from( [new Map(), new stdClass()] )->implements( '\Countable' );
2166
         *  Map::from( [new Map(), 123] )->implements( '\Countable' );
2167
         *  Map::from( [new Map(), 123] )->implements( '\Countable', true );
2168
         *  Map::from( [new Map(), 123] )->implements( '\Countable', '\RuntimeException' );
2169
         *
2170
         * Results:
2171
         *  The first example returns TRUE while the second and third one return FALSE.
2172
         *  The forth example will throw an UnexpectedValueException while the last one
2173
         *  throws a RuntimeException.
2174
         *
2175
         * @param string $interface Name of the interface that must be implemented
2176
         * @param \Throwable|bool $throw Passing TRUE or an exception name will throw the exception instead of returning FALSE
2177
         * @return bool TRUE if all entries implement the interface or FALSE if at least one doesn't
2178
         * @throws \UnexpectedValueException|\Throwable If one entry doesn't implement the interface
2179
         */
2180
        public function implements( string $interface, $throw = false ) : bool
2181
        {
2182
                foreach( $this->list() as $entry )
24✔
2183
                {
2184
                        if( !( $entry instanceof $interface ) )
24✔
2185
                        {
2186
                                if( $throw )
24✔
2187
                                {
2188
                                        $name = is_string( $throw ) ? $throw : '\UnexpectedValueException';
16✔
2189
                                        throw new $name( "Does not implement $interface: " . print_r( $entry, true ) );
16✔
2190
                                }
2191

2192
                                return false;
10✔
2193
                        }
2194
                }
2195

2196
                return true;
8✔
2197
        }
2198

2199

2200
        /**
2201
         * Tests if the passed element or elements are part of the map.
2202
         *
2203
         * Examples:
2204
         *  Map::from( ['a', 'b'] )->in( 'a' );
2205
         *  Map::from( ['a', 'b'] )->in( ['a', 'b'] );
2206
         *  Map::from( ['a', 'b'] )->in( 'x' );
2207
         *  Map::from( ['a', 'b'] )->in( ['a', 'x'] );
2208
         *  Map::from( ['1', '2'] )->in( 2, true );
2209
         *
2210
         * Results:
2211
         * The first and second example will return TRUE while the other ones will return FALSE
2212
         *
2213
         * @param mixed|array $element Element or elements to search for in the map
2214
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2215
         * @return bool TRUE if all elements are available in map, FALSE if not
2216
         */
2217
        public function in( $element, bool $strict = false ) : bool
2218
        {
2219
                if( !is_array( $element ) ) {
32✔
2220
                        return in_array( $element, $this->list(), $strict );
32✔
2221
                };
2222

2223
                foreach( $element as $entry )
8✔
2224
                {
2225
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
8✔
2226
                                return false;
8✔
2227
                        }
2228
                }
2229

2230
                return true;
8✔
2231
        }
2232

2233

2234
        /**
2235
         * Tests if the passed element or elements are part of the map.
2236
         *
2237
         * This method is an alias for in(). For performance reasons, in() should be
2238
         * preferred because it uses one method call less than includes().
2239
         *
2240
         * @param mixed|array $element Element or elements to search for in the map
2241
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2242
         * @return bool TRUE if all elements are available in map, FALSE if not
2243
         * @see in() - Underlying method with same parameters and return value but better performance
2244
         */
2245
        public function includes( $element, bool $strict = false ) : bool
2246
        {
2247
                return $this->in( $element, $strict );
8✔
2248
        }
2249

2250

2251
        /**
2252
         * Returns the numerical index of the given key.
2253
         *
2254
         * Examples:
2255
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( '8' );
2256
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( function( $key ) {
2257
         *      return $key == '8';
2258
         *  } );
2259
         *
2260
         * Results:
2261
         * Both examples will return "1" because the value "b" is at the second position
2262
         * and the returned index is zero based so the first item has the index "0".
2263
         *
2264
         * @param \Closure|string|int $value Key to search for or function with (key) parameters return TRUE if key is found
2265
         * @return int|null Position of the found value (zero based) or NULL if not found
2266
         */
2267
        public function index( $value ) : ?int
2268
        {
2269
                if( $value instanceof \Closure )
32✔
2270
                {
2271
                        $pos = 0;
16✔
2272

2273
                        foreach( $this->list() as $key => $item )
16✔
2274
                        {
2275
                                if( $value( $key ) ) {
8✔
2276
                                        return $pos;
8✔
2277
                                }
2278

2279
                                ++$pos;
8✔
2280
                        }
2281

2282
                        return null;
8✔
2283
                }
2284

2285
                $pos = array_search( $value, array_keys( $this->list() ) );
16✔
2286
                return $pos !== false ? $pos : null;
16✔
2287
        }
2288

2289

2290
        /**
2291
         * Inserts the value or values after the given element.
2292
         *
2293
         * Examples:
2294
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAfter( 'foo', 'baz' );
2295
         *  Map::from( ['foo', 'bar'] )->insertAfter( 'foo', ['baz', 'boo'] );
2296
         *  Map::from( ['foo', 'bar'] )->insertAfter( null, 'baz' );
2297
         *
2298
         * Results:
2299
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2300
         *  ['foo', 'baz', 'boo', 'bar']
2301
         *  ['foo', 'bar', 'baz']
2302
         *
2303
         * Numerical array indexes are not preserved.
2304
         *
2305
         * @param mixed $element Element after the value is inserted
2306
         * @param mixed $value Element or list of elements to insert
2307
         * @return self<int|string,mixed> Updated map for fluid interface
2308
         */
2309
        public function insertAfter( $element, $value ) : self
2310
        {
2311
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
24✔
2312
                array_splice( $this->list(), $position + 1, 0, $this->array( $value ) );
24✔
2313

2314
                return $this;
24✔
2315
        }
2316

2317

2318
        /**
2319
         * Inserts the item at the given position in the map.
2320
         *
2321
         * Examples:
2322
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 0, 'baz' );
2323
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 1, 'baz', 'c' );
2324
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 4, 'baz' );
2325
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( -1, 'baz', 'c' );
2326
         *
2327
         * Results:
2328
         *  [0 => 'baz', 'a' => 'foo', 'b' => 'bar']
2329
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2330
         *  ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']
2331
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2332
         *
2333
         * @param int $pos Position the element it should be inserted at
2334
         * @param mixed $element Element to be inserted
2335
         * @param mixed|null $key Element key or NULL to assign an integer key automatically
2336
         * @return self<int|string,mixed> Updated map for fluid interface
2337
         */
2338
        public function insertAt( int $pos, $element, $key = null ) : self
2339
        {
2340
                if( $key !== null )
40✔
2341
                {
2342
                        $list = $this->list();
16✔
2343

2344
                        $this->list = array_merge(
16✔
2345
                                array_slice( $list, 0, $pos, true ),
16✔
2346
                                [$key => $element],
16✔
2347
                                array_slice( $list, $pos, null, true )
16✔
2348
                        );
12✔
2349
                }
2350
                else
2351
                {
2352
                        array_splice( $this->list(), $pos, 0, [$element] );
24✔
2353
                }
2354

2355
                return $this;
40✔
2356
        }
2357

2358

2359
        /**
2360
         * Inserts the value or values before the given element.
2361
         *
2362
         * Examples:
2363
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertBefore( 'bar', 'baz' );
2364
         *  Map::from( ['foo', 'bar'] )->insertBefore( 'bar', ['baz', 'boo'] );
2365
         *  Map::from( ['foo', 'bar'] )->insertBefore( null, 'baz' );
2366
         *
2367
         * Results:
2368
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2369
         *  ['foo', 'baz', 'boo', 'bar']
2370
         *  ['foo', 'bar', 'baz']
2371
         *
2372
         * Numerical array indexes are not preserved.
2373
         *
2374
         * @param mixed $element Element before the value is inserted
2375
         * @param mixed $value Element or list of elements to insert
2376
         * @return self<int|string,mixed> Updated map for fluid interface
2377
         */
2378
        public function insertBefore( $element, $value ) : self
2379
        {
2380
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
24✔
2381
                array_splice( $this->list(), $position, 0, $this->array( $value ) );
24✔
2382

2383
                return $this;
24✔
2384
        }
2385

2386

2387
        /**
2388
         * Tests if the passed value or values are part of the strings in the map.
2389
         *
2390
         * Examples:
2391
         *  Map::from( ['abc'] )->inString( 'c' );
2392
         *  Map::from( ['abc'] )->inString( 'bc' );
2393
         *  Map::from( [12345] )->inString( '23' );
2394
         *  Map::from( [123.4] )->inString( 23.4 );
2395
         *  Map::from( [12345] )->inString( false );
2396
         *  Map::from( [12345] )->inString( true );
2397
         *  Map::from( [false] )->inString( false );
2398
         *  Map::from( ['abc'] )->inString( '' );
2399
         *  Map::from( [''] )->inString( false );
2400
         *  Map::from( ['abc'] )->inString( 'BC', false );
2401
         *  Map::from( ['abc', 'def'] )->inString( ['de', 'xy'] );
2402
         *  Map::from( ['abc', 'def'] )->inString( ['E', 'x'] );
2403
         *  Map::from( ['abc', 'def'] )->inString( 'E' );
2404
         *  Map::from( [23456] )->inString( true );
2405
         *  Map::from( [false] )->inString( 0 );
2406
         *
2407
         * Results:
2408
         * The first eleven examples will return TRUE while the last four will return FALSE
2409
         *
2410
         * All scalar values (bool, float, int and string) are casted to string values before
2411
         * comparing to the given value. Non-scalar values in the map are ignored.
2412
         *
2413
         * @param array|string $value Value or values to compare the map elements, will be casted to string type
2414
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
2415
         * @return bool TRUE If at least one element matches, FALSE if value is not in any string of the map
2416
         * @deprecated Use multi-byte aware strContains() instead
2417
         */
2418
        public function inString( $value, bool $case = true ) : bool
2419
        {
2420
                $fcn = $case ? 'strpos' : 'stripos';
8✔
2421

2422
                foreach( (array) $value as $val )
8✔
2423
                {
2424
                        if( (string) $val === '' ) {
8✔
2425
                                return true;
8✔
2426
                        }
2427

2428
                        foreach( $this->list() as $item )
8✔
2429
                        {
2430
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
8✔
2431
                                        return true;
8✔
2432
                                }
2433
                        }
2434
                }
2435

2436
                return false;
8✔
2437
        }
2438

2439

2440
        /**
2441
         * Returns an element by key and casts it to integer if possible.
2442
         *
2443
         * Examples:
2444
         *  Map::from( ['a' => true] )->int( 'a' );
2445
         *  Map::from( ['a' => '1'] )->int( 'a' );
2446
         *  Map::from( ['a' => 1.1] )->int( 'a' );
2447
         *  Map::from( ['a' => '10'] )->int( 'a' );
2448
         *  Map::from( ['a' => ['b' => ['c' => 1]]] )->int( 'a/b/c' );
2449
         *  Map::from( [] )->int( 'c', function() { return rand( 1, 1 ); } );
2450
         *  Map::from( [] )->int( 'a', 1 );
2451
         *
2452
         *  Map::from( [] )->int( 'b' );
2453
         *  Map::from( ['b' => ''] )->int( 'b' );
2454
         *  Map::from( ['b' => 'abc'] )->int( 'b' );
2455
         *  Map::from( ['b' => null] )->int( 'b' );
2456
         *  Map::from( ['b' => [1]] )->int( 'b' );
2457
         *  Map::from( ['b' => #resource] )->int( 'b' );
2458
         *  Map::from( ['b' => new \stdClass] )->int( 'b' );
2459
         *
2460
         *  Map::from( [] )->int( 'c', new \Exception( 'error' ) );
2461
         *
2462
         * Results:
2463
         * The first seven examples will return 1 while the 8th to 14th example
2464
         * returns 0. The last example will throw an exception.
2465
         *
2466
         * This does also work for multi-dimensional arrays by passing the keys
2467
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2468
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2469
         * public properties of objects or objects implementing __isset() and __get() methods.
2470
         *
2471
         * @param int|string $key Key or path to the requested item
2472
         * @param mixed $default Default value if key isn't found (will be casted to integer)
2473
         * @return int Value from map or default value
2474
         */
2475
        public function int( $key, $default = 0 ) : int
2476
        {
2477
                return (int) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
2478
        }
2479

2480

2481
        /**
2482
         * Returns all values in a new map that are available in both, the map and the given elements.
2483
         *
2484
         * Examples:
2485
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersect( ['bar'] );
2486
         *
2487
         * Results:
2488
         *  ['b' => 'bar']
2489
         *
2490
         * If a callback is passed, the given function will be used to compare the values.
2491
         * The function must accept two parameters (value A and B) and must return
2492
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2493
         * greater than value B. Both, a method name and an anonymous function can be passed:
2494
         *
2495
         *  Map::from( [0 => 'a'] )->intersect( [0 => 'A'], 'strcasecmp' );
2496
         *  Map::from( ['b' => 'a'] )->intersect( ['B' => 'A'], 'strcasecmp' );
2497
         *  Map::from( ['b' => 'a'] )->intersect( ['c' => 'A'], function( $valA, $valB ) {
2498
         *      return strtolower( $valA ) <=> strtolower( $valB );
2499
         *  } );
2500
         *
2501
         * All examples will return a map containing ['a'] because both contain the same
2502
         * values when compared case insensitive.
2503
         *
2504
         * The keys are preserved using this method.
2505
         *
2506
         * @param iterable<int|string,mixed> $elements List of elements
2507
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2508
         * @return self<int|string,mixed> New map
2509
         */
2510
        public function intersect( iterable $elements, ?callable $callback = null ) : self
2511
        {
2512
                $list = $this->list();
16✔
2513
                $elements = $this->array( $elements );
16✔
2514

2515
                if( $callback ) {
16✔
2516
                        return new static( array_uintersect( $list, $elements, $callback ) );
8✔
2517
                }
2518

2519
                return new static( array_intersect( $list, $elements ) );
8✔
2520
        }
2521

2522

2523
        /**
2524
         * Returns all values in a new map that are available in both, the map and the given elements while comparing the keys too.
2525
         *
2526
         * Examples:
2527
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectAssoc( new Map( ['foo', 'b' => 'bar'] ) );
2528
         *
2529
         * Results:
2530
         *  ['a' => 'foo']
2531
         *
2532
         * If a callback is passed, the given function will be used to compare the values.
2533
         * The function must accept two parameters (value A and B) and must return
2534
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2535
         * greater than value B. Both, a method name and an anonymous function can be passed:
2536
         *
2537
         *  Map::from( [0 => 'a'] )->intersectAssoc( [0 => 'A'], 'strcasecmp' );
2538
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['B' => 'A'], 'strcasecmp' );
2539
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['c' => 'A'], function( $valA, $valB ) {
2540
         *      return strtolower( $valA ) <=> strtolower( $valB );
2541
         *  } );
2542
         *
2543
         * The first example will return [0 => 'a'] because both contain the same
2544
         * values when compared case insensitive. The second and third example will return
2545
         * an empty map because the keys doesn't match ("b" vs. "B" and "b" vs. "c").
2546
         *
2547
         * The keys are preserved using this method.
2548
         *
2549
         * @param iterable<int|string,mixed> $elements List of elements
2550
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2551
         * @return self<int|string,mixed> New map
2552
         */
2553
        public function intersectAssoc( iterable $elements, ?callable $callback = null ) : self
2554
        {
2555
                $elements = $this->array( $elements );
40✔
2556

2557
                if( $callback ) {
40✔
2558
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
8✔
2559
                }
2560

2561
                return new static( array_intersect_assoc( $this->list(), $elements ) );
32✔
2562
        }
2563

2564

2565
        /**
2566
         * Returns all values in a new map that are available in both, the map and the given elements by comparing the keys only.
2567
         *
2568
         * Examples:
2569
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectKeys( new Map( ['foo', 'b' => 'baz'] ) );
2570
         *
2571
         * Results:
2572
         *  ['b' => 'bar']
2573
         *
2574
         * If a callback is passed, the given function will be used to compare the keys.
2575
         * The function must accept two parameters (key A and B) and must return
2576
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
2577
         * greater than key B. Both, a method name and an anonymous function can be passed:
2578
         *
2579
         *  Map::from( [0 => 'a'] )->intersectKeys( [0 => 'A'], 'strcasecmp' );
2580
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['B' => 'X'], 'strcasecmp' );
2581
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['c' => 'a'], function( $keyA, $keyB ) {
2582
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
2583
         *  } );
2584
         *
2585
         * The first example will return a map with [0 => 'a'] and the second one will
2586
         * return a map with ['b' => 'a'] because both contain the same keys when compared
2587
         * case insensitive. The third example will return an empty map because the keys
2588
         * doesn't match ("b" vs. "c").
2589
         *
2590
         * The keys are preserved using this method.
2591
         *
2592
         * @param iterable<int|string,mixed> $elements List of elements
2593
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
2594
         * @return self<int|string,mixed> New map
2595
         */
2596
        public function intersectKeys( iterable $elements, ?callable $callback = null ) : self
2597
        {
2598
                $elements = $this->array( $elements );
24✔
2599

2600
                if( $callback ) {
24✔
2601
                        return new static( array_intersect_ukey( $this->list(), $elements, $callback ) );
8✔
2602
                }
2603

2604
                return new static( array_intersect_key( $this->list(), $elements ) );
16✔
2605
        }
2606

2607

2608
        /**
2609
         * Tests if the map consists of the same keys and values
2610
         *
2611
         * Examples:
2612
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'] );
2613
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'], true );
2614
         *  Map::from( [1, 2] )->is( ['1', '2'] );
2615
         *
2616
         * Results:
2617
         *  The first example returns TRUE while the second and third one returns FALSE
2618
         *
2619
         * @param iterable<int|string,mixed> $list List of key/value pairs to compare with
2620
         * @param bool $strict TRUE for comparing order of elements too, FALSE for key/values only
2621
         * @return bool TRUE if given list is equal, FALSE if not
2622
         */
2623
        public function is( iterable $list, bool $strict = false ) : bool
2624
        {
2625
                $list = $this->array( $list );
24✔
2626

2627
                if( $strict ) {
24✔
2628
                        return $this->list() === $list;
16✔
2629
                }
2630

2631
                return $this->list() == $list;
8✔
2632
        }
2633

2634

2635
        /**
2636
         * Determines if the map is empty or not.
2637
         *
2638
         * Examples:
2639
         *  Map::from( [] )->isEmpty();
2640
         *  Map::from( ['a'] )->isEmpty();
2641
         *
2642
         * Results:
2643
         *  The first example returns TRUE while the second returns FALSE
2644
         *
2645
         * The method is equivalent to empty().
2646
         *
2647
         * @return bool TRUE if map is empty, FALSE if not
2648
         */
2649
        public function isEmpty() : bool
2650
        {
2651
                return empty( $this->list() );
32✔
2652
        }
2653

2654

2655
        /**
2656
         * Checks if the map contains a list of subsequentially numbered keys.
2657
         *
2658
         * Examples:
2659
         * Map::from( [] )->isList();
2660
         * Map::from( [1, 3, 2] )->isList();
2661
         * Map::from( [0 => 1, 1 => 2, 2 => 3] )->isList();
2662
         * Map::from( [1 => 1, 2 => 2, 3 => 3] )->isList();
2663
         * Map::from( [0 => 1, 2 => 2, 3 => 3] )->isList();
2664
         * Map::from( ['a' => 1, 1 => 2, 'c' => 3] )->isList();
2665
         *
2666
         * Results:
2667
         * The first three examples return TRUE while the last three return FALSE
2668
         *
2669
         * @return bool TRUE if the map is a list, FALSE if not
2670
         */
2671
        public function isList() : bool
2672
        {
2673
                $i = -1;
8✔
2674

2675
                foreach( $this->list() as $k => $v )
8✔
2676
                {
2677
                        if( $k !== ++$i ) {
8✔
2678
                                return false;
8✔
2679
                        }
2680
                }
2681

2682
                return true;
8✔
2683
        }
2684

2685

2686
        /**
2687
         * Determines if all entries are numeric values.
2688
         *
2689
         * Examples:
2690
         *  Map::from( [] )->isNumeric();
2691
         *  Map::from( [1] )->isNumeric();
2692
         *  Map::from( [1.1] )->isNumeric();
2693
         *  Map::from( [010] )->isNumeric();
2694
         *  Map::from( [0x10] )->isNumeric();
2695
         *  Map::from( [0b10] )->isNumeric();
2696
         *  Map::from( ['010'] )->isNumeric();
2697
         *  Map::from( ['10'] )->isNumeric();
2698
         *  Map::from( ['10.1'] )->isNumeric();
2699
         *  Map::from( [' 10 '] )->isNumeric();
2700
         *  Map::from( ['10e2'] )->isNumeric();
2701
         *  Map::from( ['0b10'] )->isNumeric();
2702
         *  Map::from( ['0x10'] )->isNumeric();
2703
         *  Map::from( ['null'] )->isNumeric();
2704
         *  Map::from( [null] )->isNumeric();
2705
         *  Map::from( [true] )->isNumeric();
2706
         *  Map::from( [[]] )->isNumeric();
2707
         *  Map::from( [''] )->isNumeric();
2708
         *
2709
         * Results:
2710
         *  The first eleven examples return TRUE while the last seven return FALSE
2711
         *
2712
         * @return bool TRUE if all map entries are numeric values, FALSE if not
2713
         */
2714
        public function isNumeric() : bool
2715
        {
2716
                foreach( $this->list() as $val )
8✔
2717
                {
2718
                        if( !is_numeric( $val ) ) {
8✔
2719
                                return false;
8✔
2720
                        }
2721
                }
2722

2723
                return true;
8✔
2724
        }
2725

2726

2727
        /**
2728
         * Determines if all entries are objects.
2729
         *
2730
         * Examples:
2731
         *  Map::from( [] )->isObject();
2732
         *  Map::from( [new stdClass] )->isObject();
2733
         *  Map::from( [1] )->isObject();
2734
         *
2735
         * Results:
2736
         *  The first two examples return TRUE while the last one return FALSE
2737
         *
2738
         * @return bool TRUE if all map entries are objects, FALSE if not
2739
         */
2740
        public function isObject() : bool
2741
        {
2742
                foreach( $this->list() as $val )
8✔
2743
                {
2744
                        if( !is_object( $val ) ) {
8✔
2745
                                return false;
8✔
2746
                        }
2747
                }
2748

2749
                return true;
8✔
2750
        }
2751

2752

2753
        /**
2754
         * Determines if all entries are scalar values.
2755
         *
2756
         * Examples:
2757
         *  Map::from( [] )->isScalar();
2758
         *  Map::from( [1] )->isScalar();
2759
         *  Map::from( [1.1] )->isScalar();
2760
         *  Map::from( ['abc'] )->isScalar();
2761
         *  Map::from( [true, false] )->isScalar();
2762
         *  Map::from( [new stdClass] )->isScalar();
2763
         *  Map::from( [#resource] )->isScalar();
2764
         *  Map::from( [null] )->isScalar();
2765
         *  Map::from( [[1]] )->isScalar();
2766
         *
2767
         * Results:
2768
         *  The first five examples return TRUE while the others return FALSE
2769
         *
2770
         * @return bool TRUE if all map entries are scalar values, FALSE if not
2771
         */
2772
        public function isScalar() : bool
2773
        {
2774
                foreach( $this->list() as $val )
8✔
2775
                {
2776
                        if( !is_scalar( $val ) ) {
8✔
2777
                                return false;
8✔
2778
                        }
2779
                }
2780

2781
                return true;
8✔
2782
        }
2783

2784

2785
        /**
2786
         * Determines if all entries are string values.
2787
         *
2788
         * Examples:
2789
         *  Map::from( ['abc'] )->isString();
2790
         *  Map::from( [] )->isString();
2791
         *  Map::from( [1] )->isString();
2792
         *  Map::from( [1.1] )->isString();
2793
         *  Map::from( [true, false] )->isString();
2794
         *  Map::from( [new stdClass] )->isString();
2795
         *  Map::from( [#resource] )->isString();
2796
         *  Map::from( [null] )->isString();
2797
         *  Map::from( [[1]] )->isString();
2798
         *
2799
         * Results:
2800
         *  The first two examples return TRUE while the others return FALSE
2801
         *
2802
         * @return bool TRUE if all map entries are string values, FALSE if not
2803
         */
2804
        public function isString() : bool
2805
        {
2806
                foreach( $this->list() as $val )
8✔
2807
                {
2808
                        if( !is_string( $val ) ) {
8✔
2809
                                return false;
8✔
2810
                        }
2811
                }
2812

2813
                return true;
8✔
2814
        }
2815

2816

2817
        /**
2818
         * Concatenates the string representation of all elements.
2819
         *
2820
         * Objects that implement __toString() does also work, otherwise (and in case
2821
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2822
         * empty strings.
2823
         *
2824
         * Examples:
2825
         *  Map::from( ['a', 'b', false] )->join();
2826
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
2827
         *
2828
         * Results:
2829
         * The first example will return "ab" while the second one will return "a-b--"
2830
         *
2831
         * @param string $glue Character or string added between elements
2832
         * @return string String of concatenated map elements
2833
         */
2834
        public function join( string $glue = '' ) : string
2835
        {
2836
                return implode( $glue, $this->list() );
8✔
2837
        }
2838

2839

2840
        /**
2841
         * Specifies the data which should be serialized to JSON by json_encode().
2842
         *
2843
         * Examples:
2844
         *   json_encode( Map::from( ['a', 'b'] ) );
2845
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
2846
         *
2847
         * Results:
2848
         *   ["a", "b"]
2849
         *   {"a":0,"b":1}
2850
         *
2851
         * @return array<int|string,mixed> Data to serialize to JSON
2852
         */
2853
        #[\ReturnTypeWillChange]
2854
        public function jsonSerialize()
2855
        {
2856
                return $this->list = $this->array( $this->list );
8✔
2857
        }
2858

2859

2860
        /**
2861
         * Returns the keys of the all elements in a new map object.
2862
         *
2863
         * Examples:
2864
         *  Map::from( ['a', 'b'] );
2865
         *  Map::from( ['a' => 0, 'b' => 1] );
2866
         *
2867
         * Results:
2868
         * The first example returns a map containing [0, 1] while the second one will
2869
         * return a map with ['a', 'b'].
2870
         *
2871
         * @return self<int|string,mixed> New map
2872
         */
2873
        public function keys() : self
2874
        {
2875
                return new static( array_keys( $this->list() ) );
8✔
2876
        }
2877

2878

2879
        /**
2880
         * Sorts the elements by their keys in reverse order.
2881
         *
2882
         * Examples:
2883
         *  Map::from( ['b' => 0, 'a' => 1] )->krsort();
2884
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsort();
2885
         *
2886
         * Results:
2887
         *  ['a' => 1, 'b' => 0]
2888
         *  [0 => 'b', 1 => 'a']
2889
         *
2890
         * The parameter modifies how the keys are compared. Possible values are:
2891
         * - SORT_REGULAR : compare elements normally (don't change types)
2892
         * - SORT_NUMERIC : compare elements numerically
2893
         * - SORT_STRING : compare elements as strings
2894
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2895
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2896
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2897
         *
2898
         * The keys are preserved using this method and no new map is created.
2899
         *
2900
         * @param int $options Sort options for krsort()
2901
         * @return self<int|string,mixed> Updated map for fluid interface
2902
         */
2903
        public function krsort( int $options = SORT_REGULAR ) : self
2904
        {
2905
                krsort( $this->list(), $options );
24✔
2906
                return $this;
24✔
2907
        }
2908

2909

2910
        /**
2911
         * Sorts a copy of the elements by their keys in reverse order.
2912
         *
2913
         * Examples:
2914
         *  Map::from( ['b' => 0, 'a' => 1] )->krsorted();
2915
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsorted();
2916
         *
2917
         * Results:
2918
         *  ['a' => 1, 'b' => 0]
2919
         *  [0 => 'b', 1 => 'a']
2920
         *
2921
         * The parameter modifies how the keys are compared. Possible values are:
2922
         * - SORT_REGULAR : compare elements normally (don't change types)
2923
         * - SORT_NUMERIC : compare elements numerically
2924
         * - SORT_STRING : compare elements as strings
2925
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2926
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2927
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2928
         *
2929
         * The keys are preserved using this method and a new map is created.
2930
         *
2931
         * @param int $options Sort options for krsort()
2932
         * @return self<int|string,mixed> Updated map for fluid interface
2933
         */
2934
        public function krsorted( int $options = SORT_REGULAR ) : self
2935
        {
2936
                return ( clone $this )->krsort();
8✔
2937
        }
2938

2939

2940
        /**
2941
         * Sorts the elements by their keys.
2942
         *
2943
         * Examples:
2944
         *  Map::from( ['b' => 0, 'a' => 1] )->ksort();
2945
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksort();
2946
         *
2947
         * Results:
2948
         *  ['a' => 1, 'b' => 0]
2949
         *  [0 => 'b', 1 => 'a']
2950
         *
2951
         * The parameter modifies how the keys are compared. Possible values are:
2952
         * - SORT_REGULAR : compare elements normally (don't change types)
2953
         * - SORT_NUMERIC : compare elements numerically
2954
         * - SORT_STRING : compare elements as strings
2955
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2956
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2957
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2958
         *
2959
         * The keys are preserved using this method and no new map is created.
2960
         *
2961
         * @param int $options Sort options for ksort()
2962
         * @return self<int|string,mixed> Updated map for fluid interface
2963
         */
2964
        public function ksort( int $options = SORT_REGULAR ) : self
2965
        {
2966
                ksort( $this->list(), $options );
24✔
2967
                return $this;
24✔
2968
        }
2969

2970

2971
        /**
2972
         * Sorts a copy of the elements by their keys.
2973
         *
2974
         * Examples:
2975
         *  Map::from( ['b' => 0, 'a' => 1] )->ksorted();
2976
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksorted();
2977
         *
2978
         * Results:
2979
         *  ['a' => 1, 'b' => 0]
2980
         *  [0 => 'b', 1 => 'a']
2981
         *
2982
         * The parameter modifies how the keys are compared. Possible values are:
2983
         * - SORT_REGULAR : compare elements normally (don't change types)
2984
         * - SORT_NUMERIC : compare elements numerically
2985
         * - SORT_STRING : compare elements as strings
2986
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2987
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2988
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2989
         *
2990
         * The keys are preserved using this method and no new map is created.
2991
         *
2992
         * @param int $options Sort options for ksort()
2993
         * @return self<int|string,mixed> Updated map for fluid interface
2994
         */
2995
        public function ksorted( int $options = SORT_REGULAR ) : self
2996
        {
2997
                return ( clone $this )->ksort();
8✔
2998
        }
2999

3000

3001
        /**
3002
         * Returns the last element from the map.
3003
         *
3004
         * Examples:
3005
         *  Map::from( ['a', 'b'] )->last();
3006
         *  Map::from( [] )->last( 'x' );
3007
         *  Map::from( [] )->last( new \Exception( 'error' ) );
3008
         *  Map::from( [] )->last( function() { return rand(); } );
3009
         *
3010
         * Results:
3011
         * The first example will return 'b' and the second one 'x'. The third example
3012
         * will throw the exception passed if the map contains no elements. In the
3013
         * fourth example, a random value generated by the closure function will be
3014
         * returned.
3015
         *
3016
         * @param mixed $default Default value or exception if the map contains no elements
3017
         * @return mixed Last value of map, (generated) default value or an exception
3018
         */
3019
        public function last( $default = null )
3020
        {
3021
                if( ( $value = end( $this->list() ) ) !== false ) {
40✔
3022
                        return $value;
16✔
3023
                }
3024

3025
                if( $default instanceof \Closure ) {
24✔
3026
                        return $default();
8✔
3027
                }
3028

3029
                if( $default instanceof \Throwable ) {
16✔
3030
                        throw $default;
8✔
3031
                }
3032

3033
                return $default;
8✔
3034
        }
3035

3036

3037
        /**
3038
         * Returns the last key from the map.
3039
         *
3040
         * Examples:
3041
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
3042
         *  Map::from( [] )->lastKey();
3043
         *
3044
         * Results:
3045
         * The first example will return 'b' and the second one NULL.
3046
         *
3047
         * @return mixed Last key of map or NULL if empty
3048
         */
3049
        public function lastKey()
3050
        {
3051
                $list = $this->list();
16✔
3052

3053
                if( function_exists( 'array_key_last' ) ) {
16✔
3054
                        return array_key_last( $list );
16✔
3055
                }
3056

3057
                end( $list );
×
3058
                return key( $list );
×
3059
        }
3060

3061

3062
        /**
3063
         * Removes the passed characters from the left of all strings.
3064
         *
3065
         * Examples:
3066
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
3067
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
3068
         *
3069
         * Results:
3070
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
3071
         *
3072
         * @param string $chars List of characters to trim
3073
         * @return self<int|string,mixed> Updated map for fluid interface
3074
         */
3075
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
3076
        {
3077
                foreach( $this->list() as &$entry )
8✔
3078
                {
3079
                        if( is_string( $entry ) ) {
8✔
3080
                                $entry = ltrim( $entry, $chars );
8✔
3081
                        }
3082
                }
3083

3084
                return $this;
8✔
3085
        }
3086

3087

3088
        /**
3089
         * Maps new values to the existing keys using the passed function and returns a new map for the result.
3090
         *
3091
         * Examples:
3092
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
3093
         *      return $value * 2;
3094
         *  } );
3095
         *
3096
         * Results:
3097
         *  ['a' => 4, 'b' => 8]
3098
         *
3099
         * The keys are preserved using this method.
3100
         *
3101
         * @param callable $callback Function with (value, key) parameters and returns computed result
3102
         * @return self<int|string,mixed> New map with the original keys and the computed values
3103
         * @see rekey() - Changes the keys according to the passed function
3104
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3105
         */
3106
        public function map( callable $callback ) : self
3107
        {
3108
                $list = $this->list();
8✔
3109
                $keys = array_keys( $list );
8✔
3110
                $elements = array_map( $callback, $list, $keys );
8✔
3111

3112
                return new static( array_combine( $keys, $elements ) ?: [] );
8✔
3113
        }
3114

3115

3116
        /**
3117
         * Returns the maximum value of all elements.
3118
         *
3119
         * Examples:
3120
         *  Map::from( [1, 3, 2, 5, 4] )->max()
3121
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
3122
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
3123
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
3124
         *  Map::from( [50, 10, 30] )->max( fn( $val, $key ) => $key > 0 )
3125
         *
3126
         * Results:
3127
         * The first line will return "5", the second one "foo" and the third/fourth
3128
         * one return both 50 while the last one will return 30.
3129
         *
3130
         * This does also work for multi-dimensional arrays by passing the keys
3131
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3132
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3133
         * public properties of objects or objects implementing __isset() and __get() methods.
3134
         *
3135
         * Be careful comparing elements of different types because this can have
3136
         * unpredictable results due to the PHP comparison rules:
3137
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3138
         *
3139
         * @param Closure|string|null $col Closure, key or path to the value of the nested array or object
3140
         * @return mixed Maximum value or NULL if there are no elements in the map
3141
         */
3142
        public function max( $col = null )
3143
        {
3144
                if( $col instanceof \Closure ) {
32✔
3145
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
3146
                } elseif( is_string( $col ) ) {
24✔
3147
                        $vals = $this->col( $col )->toArray();
8✔
3148
                } elseif( is_null( $col ) ) {
16✔
3149
                        $vals = $this->list();
16✔
3150
                } else {
3151
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
3152
                }
3153

3154
                return !empty( $vals ) ? max( $vals ) : null;
32✔
3155
        }
3156

3157

3158
        /**
3159
         * Merges the map with the given elements without returning a new map.
3160
         *
3161
         * Elements with the same non-numeric keys will be overwritten, elements
3162
         * with the same numeric keys will be added.
3163
         *
3164
         * Examples:
3165
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
3166
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
3167
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
3168
         *
3169
         * Results:
3170
         *  ['a', 'b', 'b', 'c']
3171
         *  ['a' => 1, 'b' => 4, 'c' => 6]
3172
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
3173
         *
3174
         * The method is similar to replace() but doesn't replace elements with
3175
         * the same numeric keys. If you want to be sure that all passed elements
3176
         * are added without replacing existing ones, use concat() instead.
3177
         *
3178
         * The keys are preserved using this method.
3179
         *
3180
         * @param iterable<int|string,mixed> $elements List of elements
3181
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
3182
         * @return self<int|string,mixed> Updated map for fluid interface
3183
         */
3184
        public function merge( iterable $elements, bool $recursive = false ) : self
3185
        {
3186
                if( $recursive ) {
24✔
3187
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
8✔
3188
                } else {
3189
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
16✔
3190
                }
3191

3192
                return $this;
24✔
3193
        }
3194

3195

3196
        /**
3197
         * Returns the minimum value of all elements.
3198
         *
3199
         * Examples:
3200
         *  Map::from( [2, 3, 1, 5, 4] )->min()
3201
         *  Map::from( ['baz', 'foo', 'bar'] )->min()
3202
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->min( 'p' )
3203
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( 'i/p' )
3204
         *  Map::from( [10, 50, 30] )->min( fn( $val, $key ) => $key > 0 )
3205
         *
3206
         * Results:
3207
         * The first line will return "1", the second one "bar", the third one
3208
         * 10, the forth and last one 30.
3209
         *
3210
         * This does also work for multi-dimensional arrays by passing the keys
3211
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3212
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3213
         * public properties of objects or objects implementing __isset() and __get() methods.
3214
         *
3215
         * Be careful comparing elements of different types because this can have
3216
         * unpredictable results due to the PHP comparison rules:
3217
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3218
         *
3219
         * @param Closure|string|null $key Closure, key or path to the value of the nested array or object
3220
         * @return mixed Minimum value or NULL if there are no elements in the map
3221
         */
3222
        public function min( $col = null )
3223
        {
3224
                if( $col instanceof \Closure ) {
32✔
3225
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
3226
                } elseif( is_string( $col ) ) {
24✔
3227
                        $vals = $this->col( $col )->toArray();
8✔
3228
                } elseif( is_null( $col ) ) {
16✔
3229
                        $vals = $this->list();
16✔
3230
                } else {
3231
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
3232
                }
3233

3234
                return !empty( $vals ) ? min( $vals ) : null;
32✔
3235
        }
3236

3237

3238
        /**
3239
         * Tests if none of the elements are part of the map.
3240
         *
3241
         * Examples:
3242
         *  Map::from( ['a', 'b'] )->none( 'x' );
3243
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
3244
         *  Map::from( ['1', '2'] )->none( 2, true );
3245
         *  Map::from( ['a', 'b'] )->none( 'a' );
3246
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
3247
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
3248
         *
3249
         * Results:
3250
         * The first three examples will return TRUE while the other ones will return FALSE
3251
         *
3252
         * @param mixed|array $element Element or elements to search for in the map
3253
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3254
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
3255
         */
3256
        public function none( $element, bool $strict = false ) : bool
3257
        {
3258
                $list = $this->list();
8✔
3259

3260
                if( !is_array( $element ) ) {
8✔
3261
                        return !in_array( $element, $list, $strict );
8✔
3262
                };
3263

3264
                foreach( $element as $entry )
8✔
3265
                {
3266
                        if( in_array( $entry, $list, $strict ) === true ) {
8✔
3267
                                return false;
8✔
3268
                        }
3269
                }
3270

3271
                return true;
8✔
3272
        }
3273

3274

3275
        /**
3276
         * Returns every nth element from the map.
3277
         *
3278
         * Examples:
3279
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
3280
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
3281
         *
3282
         * Results:
3283
         *  ['a', 'c', 'e']
3284
         *  ['b', 'd', 'f']
3285
         *
3286
         * @param int $step Step width
3287
         * @param int $offset Number of element to start from (0-based)
3288
         * @return self<int|string,mixed> New map
3289
         */
3290
        public function nth( int $step, int $offset = 0 ) : self
3291
        {
3292
                if( $step < 1 ) {
24✔
3293
                        throw new \InvalidArgumentException( 'Step width must be greater than zero' );
8✔
3294
                }
3295

3296
                if( $step === 1 ) {
16✔
3297
                        return clone $this;
8✔
3298
                }
3299

3300
                $result = [];
8✔
3301
                $list = $this->list();
8✔
3302

3303
                while( !empty( $pair = array_slice( $list, $offset, 1, true ) ) )
8✔
3304
                {
3305
                        $result += $pair;
8✔
3306
                        $offset += $step;
8✔
3307
                }
3308

3309
                return new static( $result );
8✔
3310
        }
3311

3312

3313
        /**
3314
         * Determines if an element exists at an offset.
3315
         *
3316
         * Examples:
3317
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3318
         *  isset( $map['b'] );
3319
         *  isset( $map['c'] );
3320
         *  isset( $map['d'] );
3321
         *
3322
         * Results:
3323
         *  The first isset() will return TRUE while the second and third one will return FALSE
3324
         *
3325
         * @param int|string $key Key to check for
3326
         * @return bool TRUE if key exists, FALSE if not
3327
         */
3328
        public function offsetExists( $key ) : bool
3329
        {
3330
                return isset( $this->list()[$key] );
56✔
3331
        }
3332

3333

3334
        /**
3335
         * Returns an element at a given offset.
3336
         *
3337
         * Examples:
3338
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3339
         *  $map['b'];
3340
         *
3341
         * Results:
3342
         *  $map['b'] will return 3
3343
         *
3344
         * @param int|string $key Key to return the element for
3345
         * @return mixed Value associated to the given key
3346
         */
3347
        #[\ReturnTypeWillChange]
3348
        public function offsetGet( $key )
3349
        {
3350
                return $this->list()[$key] ?? null;
40✔
3351
        }
3352

3353

3354
        /**
3355
         * Sets the element at a given offset.
3356
         *
3357
         * Examples:
3358
         *  $map = Map::from( ['a' => 1] );
3359
         *  $map['b'] = 2;
3360
         *  $map[0] = 4;
3361
         *
3362
         * Results:
3363
         *  ['a' => 1, 'b' => 2, 0 => 4]
3364
         *
3365
         * @param int|string|null $key Key to set the element for or NULL to append value
3366
         * @param mixed $value New value set for the key
3367
         */
3368
        public function offsetSet( $key, $value ) : void
3369
        {
3370
                if( $key !== null ) {
24✔
3371
                        $this->list()[$key] = $value;
16✔
3372
                } else {
3373
                        $this->list()[] = $value;
16✔
3374
                }
3375
        }
6✔
3376

3377

3378
        /**
3379
         * Unsets the element at a given offset.
3380
         *
3381
         * Examples:
3382
         *  $map = Map::from( ['a' => 1] );
3383
         *  unset( $map['a'] );
3384
         *
3385
         * Results:
3386
         *  The map will be empty
3387
         *
3388
         * @param int|string $key Key for unsetting the item
3389
         */
3390
        public function offsetUnset( $key ) : void
3391
        {
3392
                unset( $this->list()[$key] );
16✔
3393
        }
4✔
3394

3395

3396
        /**
3397
         * Returns a new map with only those elements specified by the given keys.
3398
         *
3399
         * Examples:
3400
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3401
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3402
         *
3403
         * Results:
3404
         *  ['a' => 1]
3405
         *  [0 => 'b', 1 => 'c']
3406
         *
3407
         * The keys are preserved using this method.
3408
         *
3409
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3410
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3411
         */
3412
        public function only( $keys ) : self
3413
        {
3414
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
8✔
3415
        }
3416

3417

3418
        /**
3419
         * Returns a new map with elements ordered by the passed keys.
3420
         *
3421
         * If there are less keys passed than available in the map, the remaining
3422
         * elements are removed. Otherwise, if keys are passed that are not in the
3423
         * map, they will be also available in the returned map but their value is
3424
         * NULL.
3425
         *
3426
         * Examples:
3427
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3428
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3429
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3430
         *
3431
         * Results:
3432
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3433
         *  [0 => 'b', 1 => 'c', 2 => null]
3434
         *  [0 => 'b', 1 => 'c']
3435
         *
3436
         * The keys are preserved using this method.
3437
         *
3438
         * @param iterable<mixed> $keys Keys of the elements in the required order
3439
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3440
         */
3441
        public function order( iterable $keys ) : self
3442
        {
3443
                $result = [];
8✔
3444
                $list = $this->list();
8✔
3445

3446
                foreach( $keys as $key ) {
8✔
3447
                        $result[$key] = $list[$key] ?? null;
8✔
3448
                }
3449

3450
                return new static( $result );
8✔
3451
        }
3452

3453

3454
        /**
3455
         * Fill up to the specified length with the given value
3456
         *
3457
         * In case the given number is smaller than the number of element that are
3458
         * already in the list, the map is unchanged. If the size is positive, the
3459
         * new elements are padded on the right, if it's negative then the elements
3460
         * are padded on the left.
3461
         *
3462
         * Examples:
3463
         *  Map::from( [1, 2, 3] )->pad( 5 );
3464
         *  Map::from( [1, 2, 3] )->pad( -5 );
3465
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3466
         *  Map::from( [1, 2, 3] )->pad( 2 );
3467
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3468
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3469
         *
3470
         * Results:
3471
         *  [1, 2, 3, null, null]
3472
         *  [null, null, 1, 2, 3]
3473
         *  [1, 2, 3, '0', '0']
3474
         *  [1, 2, 3]
3475
         *  [0 => 1, 1 => 2, 2 => null]
3476
         *  ['a' => 1, 'b' => 2, 0 => 3]
3477
         *
3478
         * Associative keys are preserved, numerical keys are replaced and numerical
3479
         * keys are used for the new elements.
3480
         *
3481
         * @param int $size Total number of elements that should be in the list
3482
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3483
         * @return self<int|string,mixed> New map
3484
         */
3485
        public function pad( int $size, $value = null ) : self
3486
        {
3487
                return new static( array_pad( $this->list(), $size, $value ) );
8✔
3488
        }
3489

3490

3491
        /**
3492
         * Breaks the list of elements into the given number of groups.
3493
         *
3494
         * Examples:
3495
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3496
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3497
         *                return $idx % 3;
3498
         *        } );
3499
         *
3500
         * Results:
3501
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3502
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3503
         *
3504
         * The keys of the original map are preserved in the returned map.
3505
         *
3506
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3507
         * @return self<int|string,mixed> New map
3508
         */
3509
        public function partition( $number ) : self
3510
        {
3511
                $list = $this->list();
32✔
3512

3513
                if( empty( $list ) ) {
32✔
3514
                        return new static();
8✔
3515
                }
3516

3517
                $result = [];
24✔
3518

3519
                if( $number instanceof \Closure )
24✔
3520
                {
3521
                        foreach( $list as $idx => $item ) {
8✔
3522
                                $result[$number( $item, $idx )][$idx] = $item;
8✔
3523
                        }
3524

3525
                        return new static( $result );
8✔
3526
                }
3527
                elseif( is_int( $number ) )
16✔
3528
                {
3529
                        $start = 0;
8✔
3530
                        $size = (int) ceil( count( $list ) / $number );
8✔
3531

3532
                        for( $i = 0; $i < $number; $i++ )
8✔
3533
                        {
3534
                                $result[] = array_slice( $list, $start, $size, true );
8✔
3535
                                $start += $size;
8✔
3536
                        }
3537

3538
                        return new static( $result );
8✔
3539
                }
3540

3541
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
8✔
3542
        }
3543

3544

3545
        /**
3546
         * Returns the percentage of all elements passing the test in the map.
3547
         *
3548
         * Examples:
3549
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50 );
3550
         *  Map::from( [] )->percentage( fn( $val, $key ) => true );
3551
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 100 );
3552
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 3 );
3553
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 0 );
3554
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50, -1 );
3555
         *
3556
         * Results:
3557
         * The first line will return "66.67", the second and third one "0.0", the forth
3558
         * one "33.333", the fifth one "33.0" and the last one "70.0" (66 rounded up).
3559
         *
3560
         * @param Closure $fcn Closure to filter the values in the nested array or object to compute the percentage
3561
         * @param int $precision Number of decimal digits use by the result value
3562
         * @return float Percentage of all elements passing the test in the map
3563
         */
3564
        public function percentage( \Closure $fcn, int $precision = 2 ) : float
3565
        {
3566
                $vals = array_filter( $this->list(), $fcn, ARRAY_FILTER_USE_BOTH );
8✔
3567

3568
                $cnt = count( $this->list() );
8✔
3569
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
8✔
3570
        }
3571

3572

3573
        /**
3574
         * Passes the map to the given callback and return the result.
3575
         *
3576
         * Examples:
3577
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3578
         *      return join( '-', $map->toArray() );
3579
         *  } );
3580
         *
3581
         * Results:
3582
         *  "a-b" will be returned
3583
         *
3584
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3585
         * @return mixed Result returned by the callback
3586
         */
3587
        public function pipe( \Closure $callback )
3588
        {
3589
                return $callback( $this );
8✔
3590
        }
3591

3592

3593
        /**
3594
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3595
         *
3596
         * This method is an alias for col(). For performance reasons, col() should
3597
         * be preferred because it uses one method call less than pluck().
3598
         *
3599
         * @param string|null $valuecol Name or path of the value property
3600
         * @param string|null $indexcol Name or path of the index property
3601
         * @return self<int|string,mixed> New map with mapped entries
3602
         * @see col() - Underlying method with same parameters and return value but better performance
3603
         */
3604
        public function pluck( ?string $valuecol = null, ?string $indexcol = null ) : self
3605
        {
3606
                return $this->col( $valuecol, $indexcol );
8✔
3607
        }
3608

3609

3610
        /**
3611
         * Returns and removes the last element from the map.
3612
         *
3613
         * Examples:
3614
         *  Map::from( ['a', 'b'] )->pop();
3615
         *
3616
         * Results:
3617
         *  "b" will be returned and the map only contains ['a'] afterwards
3618
         *
3619
         * @return mixed Last element of the map or null if empty
3620
         */
3621
        public function pop()
3622
        {
3623
                return array_pop( $this->list() );
16✔
3624
        }
3625

3626

3627
        /**
3628
         * Returns the numerical index of the value.
3629
         *
3630
         * Examples:
3631
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3632
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3633
         *      return $item === 'b';
3634
         *  } );
3635
         *
3636
         * Results:
3637
         * Both examples will return "1" because the value "b" is at the second position
3638
         * and the returned index is zero based so the first item has the index "0".
3639
         *
3640
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3641
         * @return int|null Position of the found value (zero based) or NULL if not found
3642
         */
3643
        public function pos( $value ) : ?int
3644
        {
3645
                $pos = 0;
120✔
3646
                $list = $this->list();
120✔
3647

3648
                if( $value instanceof \Closure )
120✔
3649
                {
3650
                        foreach( $list as $key => $item )
24✔
3651
                        {
3652
                                if( $value( $item, $key ) ) {
24✔
3653
                                        return $pos;
24✔
3654
                                }
3655

3656
                                ++$pos;
24✔
3657
                        }
3658
                }
3659

3660
                if( ( $key = array_search( $value, $list, true ) ) !== false
96✔
3661
                        && ( $pos = array_search( $key, array_keys( $list ), true ) ) !== false
96✔
3662
                ) {
3663
                        return $pos;
80✔
3664
                }
3665

3666
                return null;
16✔
3667
        }
3668

3669

3670
        /**
3671
         * Adds a prefix in front of each map entry.
3672
         *
3673
         * By defaul, nested arrays are walked recusively so all entries at all levels are prefixed.
3674
         *
3675
         * Examples:
3676
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3677
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3678
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3679
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3680
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3681
         *  } );
3682
         *
3683
         * Results:
3684
         *  The first example returns ['1-a', '1-b'] while the second one will return
3685
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3686
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3687
         *  the closure will return ['145-a', '147-b'].
3688
         *
3689
         * The keys of the original map are preserved in the returned map.
3690
         *
3691
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3692
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3693
         * @return self<int|string,mixed> Updated map for fluid interface
3694
         */
3695
        public function prefix( $prefix, ?int $depth = null ) : self
3696
        {
3697
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
6✔
3698

3699
                        foreach( $list as $key => $item )
8✔
3700
                        {
3701
                                if( is_array( $item ) ) {
8✔
3702
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
8✔
3703
                                } else {
3704
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
8✔
3705
                                }
3706
                        }
3707

3708
                        return $list;
8✔
3709
                };
8✔
3710

3711
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
8✔
3712
                return $this;
8✔
3713
        }
3714

3715

3716
        /**
3717
         * Pushes an element onto the beginning of the map without returning a new map.
3718
         *
3719
         * This method is an alias for unshift().
3720
         *
3721
         * @param mixed $value Item to add at the beginning
3722
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3723
         * @return self<int|string,mixed> Updated map for fluid interface
3724
         * @see unshift() - Underlying method with same parameters and return value but better performance
3725
         */
3726
        public function prepend( $value, $key = null ) : self
3727
        {
3728
                return $this->unshift( $value, $key );
8✔
3729
        }
3730

3731

3732
        /**
3733
         * Returns and removes an element from the map by its key.
3734
         *
3735
         * Examples:
3736
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3737
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3738
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3739
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3740
         *
3741
         * Results:
3742
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3743
         * The second one will return "none" and the map content stays untouched. If you
3744
         * pass an exception as default value, it will throw that exception if the map
3745
         * contains no elements. In the fourth example, a random value generated by the
3746
         * closure function will be returned.
3747
         *
3748
         * @param int|string $key Key to retrieve the value for
3749
         * @param mixed $default Default value if key isn't available
3750
         * @return mixed Value from map or default value
3751
         */
3752
        public function pull( $key, $default = null )
3753
        {
3754
                $value = $this->get( $key, $default );
32✔
3755
                unset( $this->list()[$key] );
24✔
3756

3757
                return $value;
24✔
3758
        }
3759

3760

3761
        /**
3762
         * Pushes an element onto the end of the map without returning a new map.
3763
         *
3764
         * Examples:
3765
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3766
         *
3767
         * Results:
3768
         *  ['a', 'b', 'aa']
3769
         *
3770
         * @param mixed $value Value to add to the end
3771
         * @return self<int|string,mixed> Updated map for fluid interface
3772
         */
3773
        public function push( $value ) : self
3774
        {
3775
                $this->list()[] = $value;
24✔
3776
                return $this;
24✔
3777
        }
3778

3779

3780
        /**
3781
         * Sets the given key and value in the map without returning a new map.
3782
         *
3783
         * This method is an alias for set(). For performance reasons, set() should be
3784
         * preferred because it uses one method call less than put().
3785
         *
3786
         * @param int|string $key Key to set the new value for
3787
         * @param mixed $value New element that should be set
3788
         * @return self<int|string,mixed> Updated map for fluid interface
3789
         * @see set() - Underlying method with same parameters and return value but better performance
3790
         */
3791
        public function put( $key, $value ) : self
3792
        {
3793
                return $this->set( $key, $value );
8✔
3794
        }
3795

3796

3797
        /**
3798
         * Returns one or more random element from the map incl. their keys.
3799
         *
3800
         * Examples:
3801
         *  Map::from( [2, 4, 8, 16] )->random();
3802
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3803
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3804
         *
3805
         * Results:
3806
         * The first example will return a map including [0 => 8] or any other value,
3807
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3808
         * and the third example will return a map of the whole list in random order. The
3809
         * less elements are in the map, the less random the order will be, especially if
3810
         * the maximum number of values is high or close to the number of elements.
3811
         *
3812
         * The keys of the original map are preserved in the returned map.
3813
         *
3814
         * @param int $max Maximum number of elements that should be returned
3815
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
3816
         * @throws \InvalidArgumentException If requested number of elements is less than 1
3817
         */
3818
        public function random( int $max = 1 ) : self
3819
        {
3820
                if( $max < 1 ) {
40✔
3821
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
8✔
3822
                }
3823

3824
                $list = $this->list();
32✔
3825

3826
                if( empty( $list ) ) {
32✔
3827
                        return new static();
8✔
3828
                }
3829

3830
                if( ( $num = count( $list ) ) < $max ) {
24✔
3831
                        $max = $num;
8✔
3832
                }
3833

3834
                $keys = array_rand( $list, $max );
24✔
3835

3836
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
24✔
3837
        }
3838

3839

3840
        /**
3841
         * Iteratively reduces the array to a single value using a callback function.
3842
         * Afterwards, the map will be empty.
3843
         *
3844
         * Examples:
3845
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
3846
         *      return $result += $value;
3847
         *  }, 10 );
3848
         *
3849
         * Results:
3850
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
3851
         *
3852
         * @param callable $callback Function with (result, value) parameters and returns result
3853
         * @param mixed $initial Initial value when computing the result
3854
         * @return mixed Value computed by the callback function
3855
         */
3856
        public function reduce( callable $callback, $initial = null )
3857
        {
3858
                return array_reduce( $this->list(), $callback, $initial );
8✔
3859
        }
3860

3861

3862
        /**
3863
         * Removes all matched elements and returns a new map.
3864
         *
3865
         * Examples:
3866
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
3867
         *      return $value < 'm';
3868
         *  } );
3869
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
3870
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
3871
         *
3872
         * Results:
3873
         *  [13 => 'm', 30 => 'z']
3874
         *  [2 => 'a', 30 => 'z']
3875
         *  [6 => null]
3876
         *
3877
         * This method is the inverse of the filter() and should return TRUE if the
3878
         * item should be removed from the returned map.
3879
         *
3880
         * If no callback is passed, all values which are NOT empty, null or false will be
3881
         * removed. The keys of the original map are preserved in the returned map.
3882
         *
3883
         * @param Closure|mixed $callback Function with (item) parameter which returns TRUE/FALSE or value to compare with
3884
         * @return self<int|string,mixed> New map
3885
         */
3886
        public function reject( $callback = true ) : self
3887
        {
3888
                $isCallable = $callback instanceof \Closure;
24✔
3889

3890
                return new static( array_filter( $this->list(), function( $value, $key ) use  ( $callback, $isCallable ) {
18✔
3891
                        return $isCallable ? !$callback( $value, $key ) : $value != $callback;
24✔
3892
                }, ARRAY_FILTER_USE_BOTH ) );
24✔
3893
        }
3894

3895

3896
        /**
3897
         * Changes the keys according to the passed function.
3898
         *
3899
         * Examples:
3900
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
3901
         *      return 'key-' . $key;
3902
         *  } );
3903
         *
3904
         * Results:
3905
         *  ['key-a' => 2, 'key-b' => 4]
3906
         *
3907
         * @param callable $callback Function with (value, key) parameters and returns new key
3908
         * @return self<int|string,mixed> New map with new keys and original values
3909
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
3910
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3911
         */
3912
        public function rekey( callable $callback ) : self
3913
        {
3914
                $list = $this->list();
8✔
3915
                $keys = array_keys( $list );
8✔
3916
                $newKeys = array_map( $callback, $list, $keys );
8✔
3917

3918
                return new static( array_combine( $newKeys, $list ) ?: [] );
8✔
3919
        }
3920

3921

3922
        /**
3923
         * Removes one or more elements from the map by its keys without returning a new map.
3924
         *
3925
         * Examples:
3926
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
3927
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
3928
         *
3929
         * Results:
3930
         * The first example will result in [2 => 'b'] while the second one resulting
3931
         * in an empty list
3932
         *
3933
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
3934
         * @return self<int|string,mixed> Updated map for fluid interface
3935
         */
3936
        public function remove( $keys ) : self
3937
        {
3938
                foreach( $this->array( $keys ) as $key ) {
40✔
3939
                        unset( $this->list()[$key] );
40✔
3940
                }
3941

3942
                return $this;
40✔
3943
        }
3944

3945

3946
        /**
3947
         * Replaces elements in the map with the given elements without returning a new map.
3948
         *
3949
         * Examples:
3950
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
3951
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
3952
         *
3953
         * Results:
3954
         *  ['a' => 2, 2 => 'b']
3955
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
3956
         *
3957
         * The method is similar to merge() but it also replaces elements with numeric
3958
         * keys. These would be added by merge() with a new numeric key.
3959
         *
3960
         * The keys are preserved using this method.
3961
         *
3962
         * @param iterable<int|string,mixed> $elements List of elements
3963
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
3964
         * @return self<int|string,mixed> Updated map for fluid interface
3965
         */
3966
        public function replace( iterable $elements, bool $recursive = true ) : self
3967
        {
3968
                if( $recursive ) {
40✔
3969
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
32✔
3970
                } else {
3971
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
8✔
3972
                }
3973

3974
                return $this;
40✔
3975
        }
3976

3977

3978
        /**
3979
         * Reverses the element order with keys without returning a new map.
3980
         *
3981
         * Examples:
3982
         *  Map::from( ['a', 'b'] )->reverse();
3983
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
3984
         *
3985
         * Results:
3986
         *  ['b', 'a']
3987
         *  ['last' => 'user', 'name' => 'test']
3988
         *
3989
         * The keys are preserved using this method.
3990
         *
3991
         * @return self<int|string,mixed> Updated map for fluid interface
3992
         * @see reversed() - Reverses the element order in a copy of the map
3993
         */
3994
        public function reverse() : self
3995
        {
3996
                $this->list = array_reverse( $this->list(), true );
32✔
3997
                return $this;
32✔
3998
        }
3999

4000

4001
        /**
4002
         * Reverses the element order in a copy of the map.
4003
         *
4004
         * Examples:
4005
         *  Map::from( ['a', 'b'] )->reversed();
4006
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reversed();
4007
         *
4008
         * Results:
4009
         *  ['b', 'a']
4010
         *  ['last' => 'user', 'name' => 'test']
4011
         *
4012
         * The keys are preserved using this method and a new map is created before reversing the elements.
4013
         * Thus, reverse() should be preferred for performance reasons if possible.
4014
         *
4015
         * @return self<int|string,mixed> New map with a reversed copy of the elements
4016
         * @see reverse() - Reverses the element order with keys without returning a new map
4017
         */
4018
        public function reversed() : self
4019
        {
4020
                return ( clone $this )->reverse();
16✔
4021
        }
4022

4023

4024
        /**
4025
         * Sorts all elements in reverse order using new keys.
4026
         *
4027
         * Examples:
4028
         *  Map::from( ['a' => 1, 'b' => 0] )->rsort();
4029
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsort();
4030
         *
4031
         * Results:
4032
         *  [0 => 1, 1 => 0]
4033
         *  [0 => 'b', 1 => 'a']
4034
         *
4035
         * The parameter modifies how the values are compared. Possible parameter values are:
4036
         * - SORT_REGULAR : compare elements normally (don't change types)
4037
         * - SORT_NUMERIC : compare elements numerically
4038
         * - SORT_STRING : compare elements as strings
4039
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4040
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4041
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4042
         *
4043
         * The keys aren't preserved and elements get a new index. No new map is created
4044
         *
4045
         * @param int $options Sort options for rsort()
4046
         * @return self<int|string,mixed> Updated map for fluid interface
4047
         */
4048
        public function rsort( int $options = SORT_REGULAR ) : self
4049
        {
4050
                rsort( $this->list(), $options );
24✔
4051
                return $this;
24✔
4052
        }
4053

4054

4055
        /**
4056
         * Sorts a copy of all elements in reverse order using new keys.
4057
         *
4058
         * Examples:
4059
         *  Map::from( ['a' => 1, 'b' => 0] )->rsorted();
4060
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsorted();
4061
         *
4062
         * Results:
4063
         *  [0 => 1, 1 => 0]
4064
         *  [0 => 'b', 1 => 'a']
4065
         *
4066
         * The parameter modifies how the values are compared. Possible parameter values are:
4067
         * - SORT_REGULAR : compare elements normally (don't change types)
4068
         * - SORT_NUMERIC : compare elements numerically
4069
         * - SORT_STRING : compare elements as strings
4070
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4071
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4072
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4073
         *
4074
         * The keys aren't preserved, elements get a new index and a new map is created.
4075
         *
4076
         * @param int $options Sort options for rsort()
4077
         * @return self<int|string,mixed> Updated map for fluid interface
4078
         */
4079
        public function rsorted( int $options = SORT_REGULAR ) : self
4080
        {
4081
                return ( clone $this )->rsort( $options );
8✔
4082
        }
4083

4084

4085
        /**
4086
         * Removes the passed characters from the right of all strings.
4087
         *
4088
         * Examples:
4089
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
4090
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
4091
         *
4092
         * Results:
4093
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
4094
         *
4095
         * @param string $chars List of characters to trim
4096
         * @return self<int|string,mixed> Updated map for fluid interface
4097
         */
4098
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
4099
        {
4100
                foreach( $this->list() as &$entry )
8✔
4101
                {
4102
                        if( is_string( $entry ) ) {
8✔
4103
                                $entry = rtrim( $entry, $chars );
8✔
4104
                        }
4105
                }
4106

4107
                return $this;
8✔
4108
        }
4109

4110

4111
        /**
4112
         * Searches the map for a given value and return the corresponding key if successful.
4113
         *
4114
         * Examples:
4115
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
4116
         *  Map::from( [1, 2, 3] )->search( '2', true );
4117
         *
4118
         * Results:
4119
         * The first example will return 1 (array index) while the second one will
4120
         * return NULL because the types doesn't match (int vs. string)
4121
         *
4122
         * @param mixed $value Item to search for
4123
         * @param bool $strict TRUE if type of the element should be checked too
4124
         * @return int|string|null Key associated to the value or null if not found
4125
         */
4126
        public function search( $value, $strict = true )
4127
        {
4128
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
8✔
4129
                        return $result;
8✔
4130
                }
4131

4132
                return null;
8✔
4133
        }
4134

4135

4136
        /**
4137
         * Sets the seperator for paths to values in multi-dimensional arrays or objects.
4138
         *
4139
         * This method only changes the separator for the current map instance. To
4140
         * change the separator for all maps created afterwards, use the static
4141
         * delimiter() method instead.
4142
         *
4143
         * Examples:
4144
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '/' )->get( 'foo/bar' );
4145
         *
4146
         * Results:
4147
         *  'baz'
4148
         *
4149
         * @param string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
4150
         * @return self<int|string,mixed> Same map for fluid interface
4151
         */
4152
        public function sep( string $char ) : self
4153
        {
4154
                $this->sep = $char;
8✔
4155
                return $this;
8✔
4156
        }
4157

4158

4159
        /**
4160
         * Sets an element in the map by key without returning a new map.
4161
         *
4162
         * Examples:
4163
         *  Map::from( ['a'] )->set( 1, 'b' );
4164
         *  Map::from( ['a'] )->set( 0, 'b' );
4165
         *
4166
         * Results:
4167
         *  ['a', 'b']
4168
         *  ['b']
4169
         *
4170
         * @param int|string $key Key to set the new value for
4171
         * @param mixed $value New element that should be set
4172
         * @return self<int|string,mixed> Updated map for fluid interface
4173
         */
4174
        public function set( $key, $value ) : self
4175
        {
4176
                $this->list()[(string) $key] = $value;
40✔
4177
                return $this;
40✔
4178
        }
4179

4180

4181
        /**
4182
         * Returns and removes the first element from the map.
4183
         *
4184
         * Examples:
4185
         *  Map::from( ['a', 'b'] )->shift();
4186
         *  Map::from( [] )->shift();
4187
         *
4188
         * Results:
4189
         * The first example returns "a" and shortens the map to ['b'] only while the
4190
         * second example will return NULL
4191
         *
4192
         * Performance note:
4193
         * The bigger the list, the higher the performance impact because shift()
4194
         * reindexes all existing elements. Usually, it's better to reverse() the list
4195
         * and pop() entries from the list afterwards if a significant number of elements
4196
         * should be removed from the list:
4197
         *
4198
         *  $map->reverse()->pop();
4199
         * instead of
4200
         *  $map->shift( 'a' );
4201
         *
4202
         * @return mixed|null Value from map or null if not found
4203
         */
4204
        public function shift()
4205
        {
4206
                return array_shift( $this->list() );
8✔
4207
        }
4208

4209

4210
        /**
4211
         * Shuffles the elements in the map without returning a new map.
4212
         *
4213
         * Examples:
4214
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle();
4215
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle( true );
4216
         *
4217
         * Results:
4218
         * The map in the first example will contain "a" and "b" in random order and
4219
         * with new keys assigned. The second call will also return all values in
4220
         * random order but preserves the keys of the original list.
4221
         *
4222
         * @param bool $assoc True to preserve keys, false to assign new keys
4223
         * @return self<int|string,mixed> Updated map for fluid interface
4224
         * @see shuffled() - Shuffles the elements in a copy of the map
4225
         */
4226
        public function shuffle( bool $assoc = false ) : self
4227
        {
4228
                if( $assoc )
24✔
4229
                {
4230
                        $list = $this->list();
8✔
4231
                        $keys = array_keys( $list );
8✔
4232
                        shuffle( $keys );
8✔
4233
                        $items = [];
8✔
4234

4235
                        foreach( $keys as $key ) {
8✔
4236
                                $items[$key] = $list[$key];
8✔
4237
                        }
4238

4239
                        $this->list = $items;
8✔
4240
                }
4241
                else
4242
                {
4243
                        shuffle( $this->list() );
16✔
4244
                }
4245

4246
                return $this;
24✔
4247
        }
4248

4249

4250
        /**
4251
         * Shuffles the elements in a copy of the map.
4252
         *
4253
         * Examples:
4254
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled();
4255
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled( true );
4256
         *
4257
         * Results:
4258
         * The map in the first example will contain "a" and "b" in random order and
4259
         * with new keys assigned. The second call will also return all values in
4260
         * random order but preserves the keys of the original list.
4261
         *
4262
         * @param bool $assoc True to preserve keys, false to assign new keys
4263
         * @return self<int|string,mixed> New map with a shuffled copy of the elements
4264
         * @see shuffle() - Shuffles the elements in the map without returning a new map
4265
         */
4266
        public function shuffled( bool $assoc = false ) : self
4267
        {
4268
                return ( clone $this )->shuffle( $assoc );
8✔
4269
        }
4270

4271

4272
        /**
4273
         * Returns a new map with the given number of items skipped.
4274
         *
4275
         * Examples:
4276
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
4277
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
4278
         *      return $item < 4;
4279
         *  } );
4280
         *
4281
         * Results:
4282
         *  [2 => 3, 3 => 4]
4283
         *  [3 => 4]
4284
         *
4285
         * The keys of the items returned in the new map are the same as in the original one.
4286
         *
4287
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4288
         * @return self<int|string,mixed> New map
4289
         */
4290
        public function skip( $offset ) : self
4291
        {
4292
                if( is_scalar( $offset ) ) {
24✔
4293
                        return new static( array_slice( $this->list(), (int) $offset, null, true ) );
8✔
4294
                }
4295

4296
                if( is_callable( $offset ) )
16✔
4297
                {
4298
                        $idx = 0;
8✔
4299
                        $list = $this->list();
8✔
4300

4301
                        foreach( $list as $key => $item )
8✔
4302
                        {
4303
                                if( !$offset( $item, $key ) ) {
8✔
4304
                                        break;
8✔
4305
                                }
4306

4307
                                ++$idx;
8✔
4308
                        }
4309

4310
                        return new static( array_slice( $list, $idx, null, true ) );
8✔
4311
                }
4312

4313
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as first argument for skip()' );
8✔
4314
        }
4315

4316

4317
        /**
4318
         * Returns a map with the slice from the original map.
4319
         *
4320
         * Examples:
4321
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
4322
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
4323
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
4324
         *
4325
         * Results:
4326
         * The first example will return ['b', 'c'] and the second one ['b'] only.
4327
         * The third example returns ['c'] because the slice starts at the second
4328
         * last value and ends before the last value.
4329
         *
4330
         * The rules for offsets are:
4331
         * - If offset is non-negative, the sequence will start at that offset
4332
         * - If offset is negative, the sequence will start that far from the end
4333
         *
4334
         * Similar for the length:
4335
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4336
         * - If the array is shorter than the length, then only the available array elements will be present
4337
         * - If length is given and is negative then the sequence will stop that many elements from the end
4338
         * - If it is omitted, then the sequence will have everything from offset up until the end
4339
         *
4340
         * The keys of the items returned in the new map are the same as in the original one.
4341
         *
4342
         * @param int $offset Number of elements to start from
4343
         * @param int|null $length Number of elements to return or NULL for no limit
4344
         * @return self<int|string,mixed> New map
4345
         */
4346
        public function slice( int $offset, ?int $length = null ) : self
4347
        {
4348
                return new static( array_slice( $this->list(), $offset, $length, true ) );
48✔
4349
        }
4350

4351

4352
        /**
4353
         * Tests if at least one element passes the test or is part of the map.
4354
         *
4355
         * Examples:
4356
         *  Map::from( ['a', 'b'] )->some( 'a' );
4357
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
4358
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
4359
         *    return $item === 'a';
4360
         *  } );
4361
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
4362
         *  Map::from( ['1', '2'] )->some( [2], true );
4363
         *
4364
         * Results:
4365
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
4366
         *
4367
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
4368
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
4369
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
4370
         */
4371
        public function some( $values, bool $strict = false ) : bool
4372
        {
4373
                $list = $this->list();
48✔
4374

4375
                if( is_iterable( $values ) )
48✔
4376
                {
4377
                        foreach( $values as $entry )
24✔
4378
                        {
4379
                                if( in_array( $entry, $list, $strict ) === true ) {
24✔
4380
                                        return true;
24✔
4381
                                }
4382
                        }
4383

4384
                        return false;
16✔
4385
                }
4386
                elseif( is_callable( $values ) )
32✔
4387
                {
4388
                        foreach( $list as $key => $item )
16✔
4389
                        {
4390
                                if( $values( $item, $key ) ) {
16✔
4391
                                        return true;
16✔
4392
                                }
4393
                        }
4394
                }
4395
                elseif( in_array( $values, $list, $strict ) === true )
24✔
4396
                {
4397
                        return true;
24✔
4398
                }
4399

4400
                return false;
24✔
4401
        }
4402

4403

4404
        /**
4405
         * Sorts all elements in-place using new keys.
4406
         *
4407
         * Examples:
4408
         *  Map::from( ['a' => 1, 'b' => 0] )->sort();
4409
         *  Map::from( [0 => 'b', 1 => 'a'] )->sort();
4410
         *
4411
         * Results:
4412
         *  [0 => 0, 1 => 1]
4413
         *  [0 => 'a', 1 => 'b']
4414
         *
4415
         * The parameter modifies how the values are compared. Possible parameter values are:
4416
         * - SORT_REGULAR : compare elements normally (don't change types)
4417
         * - SORT_NUMERIC : compare elements numerically
4418
         * - SORT_STRING : compare elements as strings
4419
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4420
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4421
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4422
         *
4423
         * The keys aren't preserved and elements get a new index. No new map is created.
4424
         *
4425
         * @param int $options Sort options for PHP sort()
4426
         * @return self<int|string,mixed> Updated map for fluid interface
4427
         * @see sorted() - Sorts elements in a copy of the map
4428
         */
4429
        public function sort( int $options = SORT_REGULAR ) : self
4430
        {
4431
                sort( $this->list(), $options );
40✔
4432
                return $this;
40✔
4433
        }
4434

4435

4436
        /**
4437
         * Sorts the elements in a copy of the map using new keys.
4438
         *
4439
         * Examples:
4440
         *  Map::from( ['a' => 1, 'b' => 0] )->sorted();
4441
         *  Map::from( [0 => 'b', 1 => 'a'] )->sorted();
4442
         *
4443
         * Results:
4444
         *  [0 => 0, 1 => 1]
4445
         *  [0 => 'a', 1 => 'b']
4446
         *
4447
         * The parameter modifies how the values are compared. Possible parameter values are:
4448
         * - SORT_REGULAR : compare elements normally (don't change types)
4449
         * - SORT_NUMERIC : compare elements numerically
4450
         * - SORT_STRING : compare elements as strings
4451
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4452
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4453
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4454
         *
4455
         * The keys aren't preserved and elements get a new index and a new map is created before sorting the elements.
4456
         * Thus, sort() should be preferred for performance reasons if possible. A new map is created by calling this method.
4457
         *
4458
         * @param int $options Sort options for PHP sort()
4459
         * @return self<int|string,mixed> New map with a sorted copy of the elements
4460
         * @see sort() - Sorts elements in-place in the original map
4461
         */
4462
        public function sorted( int $options = SORT_REGULAR ) : self
4463
        {
4464
                return ( clone $this )->sort( $options );
16✔
4465
        }
4466

4467

4468
        /**
4469
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4470
         *
4471
         * Examples:
4472
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4473
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4474
         *
4475
         * Results:
4476
         * The first example removes all entries after "a", so only ['a'] will be left
4477
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4478
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4479
         * ['a', 'x', 'y', 'c'] afterwards.
4480
         *
4481
         * The rules for offsets are:
4482
         * - If offset is non-negative, the sequence will start at that offset
4483
         * - If offset is negative, the sequence will start that far from the end
4484
         *
4485
         * Similar for the length:
4486
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4487
         * - If the array is shorter than the length, then only the available array elements will be present
4488
         * - If length is given and is negative then the sequence will stop that many elements from the end
4489
         * - If it is omitted, then the sequence will have everything from offset up until the end
4490
         *
4491
         * Numerical array indexes are NOT preserved.
4492
         *
4493
         * @param int $offset Number of elements to start from
4494
         * @param int|null $length Number of elements to remove, NULL for all
4495
         * @param mixed $replacement List of elements to insert
4496
         * @return self<int|string,mixed> New map
4497
         */
4498
        public function splice( int $offset, ?int $length = null, $replacement = [] ) : self
4499
        {
4500
                if( $length === null ) {
40✔
4501
                        $length = count( $this->list() );
16✔
4502
                }
4503

4504
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
40✔
4505
        }
4506

4507

4508
        /**
4509
         * Returns the strings after the passed value.
4510
         *
4511
         * Examples:
4512
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4513
         *  Map::from( ['abc'] )->strAfter( '' );
4514
         *  Map::from( ['abc'] )->strAfter( 'b' );
4515
         *  Map::from( ['abc'] )->strAfter( 'c' );
4516
         *  Map::from( ['abc'] )->strAfter( 'x' );
4517
         *  Map::from( [''] )->strAfter( '' );
4518
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4519
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4520
         *
4521
         * Results:
4522
         *  ['üß']
4523
         *  ['abc']
4524
         *  ['c']
4525
         *  ['']
4526
         *  []
4527
         *  []
4528
         *  ['1', '1', '1']
4529
         *  ['0', '0']
4530
         *
4531
         * All scalar values (bool, int, float, string) will be converted to strings.
4532
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4533
         *
4534
         * @param string $value Character or string to search for
4535
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4536
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4537
         * @return self<int|string,mixed> New map
4538
         */
4539
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4540
        {
4541
                $list = [];
8✔
4542
                $len = mb_strlen( $value );
8✔
4543
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
8✔
4544

4545
                foreach( $this->list() as $key => $entry )
8✔
4546
                {
4547
                        if( is_scalar( $entry ) )
8✔
4548
                        {
4549
                                $pos = null;
8✔
4550
                                $str = (string) $entry;
8✔
4551

4552
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4553
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
8✔
4554
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4555
                                        $list[$key] = $str;
8✔
4556
                                }
4557
                        }
4558
                }
4559

4560
                return new static( $list );
8✔
4561
        }
4562

4563

4564
        /**
4565
         * Returns the strings before the passed value.
4566
         *
4567
         * Examples:
4568
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4569
         *  Map::from( ['abc'] )->strBefore( '' );
4570
         *  Map::from( ['abc'] )->strBefore( 'b' );
4571
         *  Map::from( ['abc'] )->strBefore( 'a' );
4572
         *  Map::from( ['abc'] )->strBefore( 'x' );
4573
         *  Map::from( [''] )->strBefore( '' );
4574
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4575
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4576
         *
4577
         * Results:
4578
         *  ['äö']
4579
         *  ['abc']
4580
         *  ['a']
4581
         *  ['']
4582
         *  []
4583
         *  []
4584
         *  ['1', '1', '1']
4585
         *  ['0', '0']
4586
         *
4587
         * All scalar values (bool, int, float, string) will be converted to strings.
4588
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4589
         *
4590
         * @param string $value Character or string to search for
4591
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4592
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4593
         * @return self<int|string,mixed> New map
4594
         */
4595
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4596
        {
4597
                $list = [];
8✔
4598
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
8✔
4599

4600
                foreach( $this->list() as $key => $entry )
8✔
4601
                {
4602
                        if( is_scalar( $entry ) )
8✔
4603
                        {
4604
                                $pos = null;
8✔
4605
                                $str = (string) $entry;
8✔
4606

4607
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4608
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
8✔
4609
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4610
                                        $list[$key] = $str;
8✔
4611
                                } else {
1✔
4612
                                }
4613
                        }
4614
                }
4615

4616
                return new static( $list );
8✔
4617
        }
4618

4619

4620
        /**
4621
         * Tests if at least one of the passed strings is part of at least one entry.
4622
         *
4623
         * Examples:
4624
         *  Map::from( ['abc'] )->strContains( '' );
4625
         *  Map::from( ['abc'] )->strContains( 'a' );
4626
         *  Map::from( ['abc'] )->strContains( 'bc' );
4627
         *  Map::from( [12345] )->strContains( '23' );
4628
         *  Map::from( [123.4] )->strContains( 23.4 );
4629
         *  Map::from( [12345] )->strContains( false );
4630
         *  Map::from( [12345] )->strContains( true );
4631
         *  Map::from( [false] )->strContains( false );
4632
         *  Map::from( [''] )->strContains( false );
4633
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4634
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4635
         *
4636
         *  Map::from( ['abc'] )->strContains( 'd' );
4637
         *  Map::from( ['abc'] )->strContains( 'cb' );
4638
         *  Map::from( [23456] )->strContains( true );
4639
         *  Map::from( [false] )->strContains( 0 );
4640
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4641
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4642
         *
4643
         * Results:
4644
         * The first eleven examples will return TRUE while the last six will return FALSE.
4645
         *
4646
         * @param array|string $value The string or list of strings to search for in each entry
4647
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4648
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4649
         * @todo 4.0 Add $case parameter at second position
4650
         */
4651
        public function strContains( $value, string $encoding = 'UTF-8' ) : bool
4652
        {
4653
                foreach( $this->list() as $entry )
8✔
4654
                {
4655
                        $entry = (string) $entry;
8✔
4656

4657
                        foreach( (array) $value as $str )
8✔
4658
                        {
4659
                                $str = (string) $str;
8✔
4660

4661
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4662
                                        return true;
8✔
4663
                                }
4664
                        }
4665
                }
4666

4667
                return false;
8✔
4668
        }
4669

4670

4671
        /**
4672
         * Tests if all of the entries contains one of the passed strings.
4673
         *
4674
         * Examples:
4675
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
4676
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
4677
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
4678
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
4679
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
4680
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
4681
         *  Map::from( ['', false] )->strContainsAll( false );
4682
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
4683
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
4684
         *
4685
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
4686
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
4687
         *  Map::from( [23456, '123'] )->strContainsAll( true );
4688
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
4689
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
4690
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
4691
         *
4692
         * Results:
4693
         * The first nine examples will return TRUE while the last six will return FALSE.
4694
         *
4695
         * @param array|string $value The string or list of strings to search for in each entry
4696
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4697
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
4698
         * @todo 4.0 Add $case parameter at second position
4699
         */
4700
        public function strContainsAll( $value, string $encoding = 'UTF-8' ) : bool
4701
        {
4702
                $list = [];
8✔
4703

4704
                foreach( $this->list() as $entry )
8✔
4705
                {
4706
                        $entry = (string) $entry;
8✔
4707
                        $list[$entry] = 0;
8✔
4708

4709
                        foreach( (array) $value as $str )
8✔
4710
                        {
4711
                                $str = (string) $str;
8✔
4712

4713
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4714
                                        $list[$entry] = 1; break;
8✔
4715
                                }
4716
                        }
4717
                }
4718

4719
                return array_sum( $list ) === count( $list );
8✔
4720
        }
4721

4722

4723
        /**
4724
         * Tests if at least one of the entries ends with one of the passed strings.
4725
         *
4726
         * Examples:
4727
         *  Map::from( ['abc'] )->strEnds( '' );
4728
         *  Map::from( ['abc'] )->strEnds( 'c' );
4729
         *  Map::from( ['abc'] )->strEnds( 'bc' );
4730
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
4731
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
4732
         *  Map::from( ['abc'] )->strEnds( 'a' );
4733
         *  Map::from( ['abc'] )->strEnds( 'cb' );
4734
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
4735
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
4736
         *
4737
         * Results:
4738
         * The first five examples will return TRUE while the last four will return FALSE.
4739
         *
4740
         * @param array|string $value The string or strings to search for in each entry
4741
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4742
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
4743
         * @todo 4.0 Add $case parameter at second position
4744
         */
4745
        public function strEnds( $value, string $encoding = 'UTF-8' ) : bool
4746
        {
4747
                foreach( $this->list() as $entry )
8✔
4748
                {
4749
                        $entry = (string) $entry;
8✔
4750

4751
                        foreach( (array) $value as $str )
8✔
4752
                        {
4753
                                $len = mb_strlen( (string) $str );
8✔
4754

4755
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4756
                                        return true;
8✔
4757
                                }
4758
                        }
4759
                }
4760

4761
                return false;
8✔
4762
        }
4763

4764

4765
        /**
4766
         * Tests if all of the entries ends with at least one of the passed strings.
4767
         *
4768
         * Examples:
4769
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
4770
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
4771
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
4772
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
4773
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
4774
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
4775
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
4776
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
4777
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
4778
         *
4779
         * Results:
4780
         * The first five examples will return TRUE while the last four will return FALSE.
4781
         *
4782
         * @param array|string $value The string or strings to search for in each entry
4783
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4784
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4785
         * @todo 4.0 Add $case parameter at second position
4786
         */
4787
        public function strEndsAll( $value, string $encoding = 'UTF-8' ) : bool
4788
        {
4789
                $list = [];
8✔
4790

4791
                foreach( $this->list() as $entry )
8✔
4792
                {
4793
                        $entry = (string) $entry;
8✔
4794
                        $list[$entry] = 0;
8✔
4795

4796
                        foreach( (array) $value as $str )
8✔
4797
                        {
4798
                                $len = mb_strlen( (string) $str );
8✔
4799

4800
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4801
                                        $list[$entry] = 1; break;
8✔
4802
                                }
4803
                        }
4804
                }
4805

4806
                return array_sum( $list ) === count( $list );
8✔
4807
        }
4808

4809

4810
        /**
4811
         * Returns an element by key and casts it to string if possible.
4812
         *
4813
         * Examples:
4814
         *  Map::from( ['a' => true] )->string( 'a' );
4815
         *  Map::from( ['a' => 1] )->string( 'a' );
4816
         *  Map::from( ['a' => 1.1] )->string( 'a' );
4817
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
4818
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
4819
         *  Map::from( [] )->string( 'a', function() { return 'no'; } );
4820
         *
4821
         *  Map::from( [] )->string( 'b' );
4822
         *  Map::from( ['b' => ''] )->string( 'b' );
4823
         *  Map::from( ['b' => null] )->string( 'b' );
4824
         *  Map::from( ['b' => [true]] )->string( 'b' );
4825
         *  Map::from( ['b' => resource] )->string( 'b' );
4826
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
4827
         *
4828
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
4829
         *
4830
         * Results:
4831
         * The first six examples will return the value as string while the 9th to 12th
4832
         * example returns an empty string. The last example will throw an exception.
4833
         *
4834
         * This does also work for multi-dimensional arrays by passing the keys
4835
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
4836
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
4837
         * public properties of objects or objects implementing __isset() and __get() methods.
4838
         *
4839
         * @param int|string $key Key or path to the requested item
4840
         * @param mixed $default Default value if key isn't found (will be casted to bool)
4841
         * @return string Value from map or default value
4842
         */
4843
        public function string( $key, $default = '' ) : string
4844
        {
4845
                return (string) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
4846
        }
4847

4848

4849
        /**
4850
         * Converts all alphabetic characters in strings to lower case.
4851
         *
4852
         * Examples:
4853
         *  Map::from( ['My String'] )->strLower();
4854
         *  Map::from( ['Τάχιστη'] )->strLower();
4855
         *  Map::from( ['Äpfel', 'Birnen'] )->strLower( 'ISO-8859-1' );
4856
         *  Map::from( [123] )->strLower();
4857
         *  Map::from( [new stdClass] )->strLower();
4858
         *
4859
         * Results:
4860
         * The first example will return ["my string"], the second one ["τάχιστη"] and
4861
         * the third one ["äpfel", "birnen"]. The last two strings will be unchanged.
4862
         *
4863
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4864
         * @return self<int|string,mixed> Updated map for fluid interface
4865
         */
4866
        public function strLower( string $encoding = 'UTF-8' ) : self
4867
        {
4868
                foreach( $this->list() as &$entry )
8✔
4869
                {
4870
                        if( is_string( $entry ) ) {
8✔
4871
                                $entry = mb_strtolower( $entry, $encoding );
8✔
4872
                        }
4873
                }
4874

4875
                return $this;
8✔
4876
        }
4877

4878

4879
        /**
4880
         * Replaces all occurrences of the search string with the replacement string.
4881
         *
4882
         * Examples:
4883
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
4884
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
4885
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
4886
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
4887
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
4888
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
4889
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
4890
         *
4891
         * Restults:
4892
         * ['google.de', 'aimeos.de']
4893
         * ['google.de', 'aimeos.de']
4894
         * ['google.de', 'aimeos']
4895
         * ['google.fr', 'aimeos.de']
4896
         * ['google.de', 'aimeos.de']
4897
         * ['google.de', 'aimeos.de', 123]
4898
         * ['GOOGLE.de', 'AIMEOS.de']
4899
         *
4900
         * If you use an array of strings for search or search/replacement, the order of
4901
         * the strings matters! Each search string found is replaced by the corresponding
4902
         * replacement string at the same position.
4903
         *
4904
         * In case of array parameters and if the number of replacement strings is less
4905
         * than the number of search strings, the search strings with no corresponding
4906
         * replacement string are replaced with empty strings. Replacement strings with
4907
         * no corresponding search string are ignored.
4908
         *
4909
         * An array parameter for the replacements is only allowed if the search parameter
4910
         * is an array of strings too!
4911
         *
4912
         * Because the method replaces from left to right, it might replace a previously
4913
         * inserted value when doing multiple replacements. Entries which are non-string
4914
         * values are left untouched.
4915
         *
4916
         * @param array|string $search String or list of strings to search for
4917
         * @param array|string $replace String or list of strings of replacement strings
4918
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
4919
         * @return self<int|string,mixed> Updated map for fluid interface
4920
         */
4921
        public function strReplace( $search, $replace, bool $case = false ) : self
4922
        {
4923
                $fcn = $case ? 'str_ireplace' : 'str_replace';
8✔
4924

4925
                foreach( $this->list() as &$entry )
8✔
4926
                {
4927
                        if( is_string( $entry ) ) {
8✔
4928
                                $entry = $fcn( $search, $replace, $entry );
8✔
4929
                        }
4930
                }
4931

4932
                return $this;
8✔
4933
        }
4934

4935

4936
        /**
4937
         * Tests if at least one of the entries starts with at least one of the passed strings.
4938
         *
4939
         * Examples:
4940
         *  Map::from( ['abc'] )->strStarts( '' );
4941
         *  Map::from( ['abc'] )->strStarts( 'a' );
4942
         *  Map::from( ['abc'] )->strStarts( 'ab' );
4943
         *  Map::from( ['abc'] )->strStarts( ['a', 'b'] );
4944
         *  Map::from( ['abc'] )->strStarts( 'ab', 'ASCII' );
4945
         *  Map::from( ['abc'] )->strStarts( 'b' );
4946
         *  Map::from( ['abc'] )->strStarts( 'bc' );
4947
         *  Map::from( ['abc'] )->strStarts( ['b', 'c'] );
4948
         *  Map::from( ['abc'] )->strStarts( 'bc', 'ASCII' );
4949
         *
4950
         * Results:
4951
         * The first five examples will return TRUE while the last four will return FALSE.
4952
         *
4953
         * @param array|string $value The string or strings to search for in each entry
4954
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4955
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4956
         * @todo 4.0 Add $case parameter at second position
4957
         */
4958
        public function strStarts( $value, string $encoding = 'UTF-8' ) : bool
4959
        {
4960
                foreach( $this->list() as $entry )
8✔
4961
                {
4962
                        $entry = (string) $entry;
8✔
4963

4964
                        foreach( (array) $value as $str )
8✔
4965
                        {
4966
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
4967
                                        return true;
8✔
4968
                                }
4969
                        }
4970
                }
4971

4972
                return false;
8✔
4973
        }
4974

4975

4976
        /**
4977
         * Tests if all of the entries starts with one of the passed strings.
4978
         *
4979
         * Examples:
4980
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
4981
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
4982
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
4983
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
4984
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
4985
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
4986
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
4987
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
4988
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
4989
         *
4990
         * Results:
4991
         * The first five examples will return TRUE while the last four will return FALSE.
4992
         *
4993
         * @param array|string $value The string or strings to search for in each entry
4994
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4995
         * @return bool TRUE if one of the entries starts with one of the strings, FALSE if not
4996
         * @todo 4.0 Add $case parameter at second position
4997
         */
4998
        public function strStartsAll( $value, string $encoding = 'UTF-8' ) : bool
4999
        {
5000
                $list = [];
8✔
5001

5002
                foreach( $this->list() as $entry )
8✔
5003
                {
5004
                        $entry = (string) $entry;
8✔
5005
                        $list[$entry] = 0;
8✔
5006

5007
                        foreach( (array) $value as $str )
8✔
5008
                        {
5009
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
5010
                                        $list[$entry] = 1; break;
8✔
5011
                                }
5012
                        }
5013
                }
5014

5015
                return array_sum( $list ) === count( $list );
8✔
5016
        }
5017

5018

5019
        /**
5020
         * Converts all alphabetic characters in strings to upper case.
5021
         *
5022
         * Examples:
5023
         *  Map::from( ['My String'] )->strUpper();
5024
         *  Map::from( ['τάχιστη'] )->strUpper();
5025
         *  Map::from( ['äpfel', 'birnen'] )->strUpper( 'ISO-8859-1' );
5026
         *  Map::from( [123] )->strUpper();
5027
         *  Map::from( [new stdClass] )->strUpper();
5028
         *
5029
         * Results:
5030
         * The first example will return ["MY STRING"], the second one ["ΤΆΧΙΣΤΗ"] and
5031
         * the third one ["ÄPFEL", "BIRNEN"]. The last two strings will be unchanged.
5032
         *
5033
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5034
         * @return self<int|string,mixed> Updated map for fluid interface
5035
         */
5036
        public function strUpper( string $encoding = 'UTF-8' ) :self
5037
        {
5038
                foreach( $this->list() as &$entry )
8✔
5039
                {
5040
                        if( is_string( $entry ) ) {
8✔
5041
                                $entry = mb_strtoupper( $entry, $encoding );
8✔
5042
                        }
5043
                }
5044

5045
                return $this;
8✔
5046
        }
5047

5048

5049
        /**
5050
         * Adds a suffix at the end of each map entry.
5051
         *
5052
         * By defaul, nested arrays are walked recusively so all entries at all levels are suffixed.
5053
         *
5054
         * Examples:
5055
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
5056
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
5057
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
5058
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
5059
         *      return '-' . ( ord( $item ) + ord( $key ) );
5060
         *  } );
5061
         *
5062
         * Results:
5063
         *  The first example returns ['a-1', 'b-1'] while the second one will return
5064
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
5065
         *  level only so it will return ['a-1', ['b']]. The forth example passing
5066
         *  the closure will return ['a-145', 'b-147'].
5067
         *
5068
         * The keys are preserved using this method.
5069
         *
5070
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
5071
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
5072
         * @return self<int|string,mixed> Updated map for fluid interface
5073
         */
5074
        public function suffix( $suffix, ?int $depth = null ) : self
5075
        {
5076
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
6✔
5077

5078
                        foreach( $list as $key => $item )
8✔
5079
                        {
5080
                                if( is_array( $item ) ) {
8✔
5081
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
8✔
5082
                                } else {
5083
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
8✔
5084
                                }
5085
                        }
5086

5087
                        return $list;
8✔
5088
                };
8✔
5089

5090
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
8✔
5091
                return $this;
8✔
5092
        }
5093

5094

5095
        /**
5096
         * Returns the sum of all integer and float values in the map.
5097
         *
5098
         * Examples:
5099
         *  Map::from( [1, 3, 5] )->sum();
5100
         *  Map::from( [1, 'sum', 5] )->sum();
5101
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
5102
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
5103
         *  Map::from( [30, 50, 10] )->sum( fn( $val, $key ) => $val < 50 );
5104
         *
5105
         * Results:
5106
         * The first line will return "9", the second one "6", the third one "90"
5107
         * the forth on "80" and the last one "40".
5108
         *
5109
         * NULL values are treated as 0, non-numeric values will generate an error.
5110
         *
5111
         * NULL values are treated as 0, non-numeric values will generate an error.
5112
         *
5113
         * This does also work for multi-dimensional arrays by passing the keys
5114
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5115
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5116
         * public properties of objects or objects implementing __isset() and __get() methods.
5117
         *
5118
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to sum up
5119
         * @return float Sum of all elements or 0 if there are no elements in the map
5120
         */
5121
        public function sum( $col = null ) : float
5122
        {
5123
                if( $col instanceof \Closure ) {
24✔
5124
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
5125
                } elseif( is_string( $col ) ) {
16✔
5126
                        $vals = $this->col( $col )->toArray();
8✔
5127
                } elseif( is_null( $col ) ) {
8✔
5128
                        $vals = $this->list();
8✔
5129
                } else {
5130
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
5131
                }
5132

5133
                return array_sum( $vals );
24✔
5134
        }
5135

5136

5137
        /**
5138
         * Returns a new map with the given number of items.
5139
         *
5140
         * The keys of the items returned in the new map are the same as in the original one.
5141
         *
5142
         * Examples:
5143
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
5144
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
5145
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
5146
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
5147
         *      return $item < 2;
5148
         *  } );
5149
         *
5150
         * Results:
5151
         *  [0 => 1, 1 => 2]
5152
         *  [1 => 2, 2 => 3]
5153
         *  [2 => 3, 3 => 4]
5154
         *  [1 => 2, 2 => 3]
5155
         *
5156
         * The keys of the items returned in the new map are the same as in the original one.
5157
         *
5158
         * @param int $size Number of items to return
5159
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5160
         * @return self<int|string,mixed> New map
5161
         */
5162
        public function take( int $size, $offset = 0 ) : self
5163
        {
5164
                $list = $this->list();
40✔
5165

5166
                if( is_scalar( $offset ) ) {
40✔
5167
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
24✔
5168
                }
5169

5170
                if( is_callable( $offset ) )
16✔
5171
                {
5172
                        $idx = 0;
8✔
5173

5174
                        foreach( $list as $key => $item )
8✔
5175
                        {
5176
                                if( !$offset( $item, $key ) ) {
8✔
5177
                                        break;
8✔
5178
                                }
5179

5180
                                ++$idx;
8✔
5181
                        }
5182

5183
                        return new static( array_slice( $list, $idx, $size, true ) );
8✔
5184
                }
5185

5186
                throw new \InvalidArgumentException( 'Only an integer or a closure is allowed as second argument for take()' );
8✔
5187
        }
5188

5189

5190
        /**
5191
         * Passes a clone of the map to the given callback.
5192
         *
5193
         * Use it to "tap" into a chain of methods to check the state between two
5194
         * method calls. The original map is not altered by anything done in the
5195
         * callback.
5196
         *
5197
         * Examples:
5198
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
5199
         *    print_r( $map->remove( 0 )->toArray() );
5200
         *  } )->first();
5201
         *
5202
         * Results:
5203
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
5204
         * then prints the items without the first (`[2, 3]`) in the function passed
5205
         * to `tap()` and returns the first item ("1") at the end.
5206
         *
5207
         * @param callable $callback Function receiving ($map) parameter
5208
         * @return self<int|string,mixed> Same map for fluid interface
5209
         */
5210
        public function tap( callable $callback ) : self
5211
        {
5212
                $callback( clone $this );
8✔
5213
                return $this;
8✔
5214
        }
5215

5216

5217
        /**
5218
         * Returns the elements as a plain array.
5219
         *
5220
         * @return array<int|string,mixed> Plain array
5221
         */
5222
        public function to() : array
5223
        {
5224
                return $this->list = $this->array( $this->list );
8✔
5225
        }
5226

5227

5228
        /**
5229
         * Returns the elements as a plain array.
5230
         *
5231
         * @return array<int|string,mixed> Plain array
5232
         */
5233
        public function toArray() : array
5234
        {
5235
                return $this->list = $this->array( $this->list );
1,808✔
5236
        }
5237

5238

5239
        /**
5240
         * Returns the elements encoded as JSON string.
5241
         *
5242
         * There are several options available to modify the JSON output:
5243
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5244
         * The parameter can be a single JSON_* constant or a bitmask of several
5245
         * constants combine by bitwise OR (|), e.g.:
5246
         *
5247
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5248
         *
5249
         * @param int $options Combination of JSON_* constants
5250
         * @return string|null Array encoded as JSON string or NULL on failure
5251
         */
5252
        public function toJson( int $options = 0 ) : ?string
5253
        {
5254
                $result = json_encode( $this->list(), $options );
16✔
5255
                return $result !== false ? $result : null;
16✔
5256
        }
5257

5258

5259
        /**
5260
         * Reverses the element order in a copy of the map (alias).
5261
         *
5262
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5263
         * preferred because it uses one method call less than toReversed().
5264
         *
5265
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5266
         * @see reversed() - Underlying method with same parameters and return value but better performance
5267
         */
5268
        public function toReversed() : self
5269
        {
5270
                return $this->reversed();
8✔
5271
        }
5272

5273

5274
        /**
5275
         * Sorts the elements in a copy of the map using new keys (alias).
5276
         *
5277
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5278
         * preferred because it uses one method call less than toSorted().
5279
         *
5280
         * @param int $options Sort options for PHP sort()
5281
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5282
         * @see sorted() - Underlying method with same parameters and return value but better performance
5283
         */
5284
        public function toSorted( int $options = SORT_REGULAR ) : self
5285
        {
5286
                return $this->sorted( $options );
8✔
5287
        }
5288

5289

5290
        /**
5291
         * Creates a HTTP query string from the map elements.
5292
         *
5293
         * Examples:
5294
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5295
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5296
         *
5297
         * Results:
5298
         *  a=1&b=2
5299
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5300
         *
5301
         * @return string Parameter string for GET requests
5302
         */
5303
        public function toUrl() : string
5304
        {
5305
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
16✔
5306
        }
5307

5308

5309
        /**
5310
         * Creates new key/value pairs using the passed function and returns a new map for the result.
5311
         *
5312
         * Examples:
5313
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5314
         *      return [$key . '-2' => $value * 2];
5315
         *  } );
5316
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5317
         *      return [$key => $value * 2, $key . $key => $value * 4];
5318
         *  } );
5319
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5320
         *      return $key < 'b' ? [$key => $value * 2] : null;
5321
         *  } );
5322
         *  Map::from( ['la' => 2, 'le' => 4, 'li' => 6] )->transform( function( $value, $key ) {
5323
         *      return [$key[0] => $value * 2];
5324
         *  } );
5325
         *
5326
         * Results:
5327
         *  ['a-2' => 4, 'b-2' => 8]
5328
         *  ['a' => 4, 'aa' => 8, 'b' => 8, 'bb' => 16]
5329
         *  ['a' => 4]
5330
         *  ['l' => 12]
5331
         *
5332
         * If a key is returned twice, the last value will overwrite previous values.
5333
         *
5334
         * @param \Closure $callback Function with (value, key) parameters and returns an array of new key/value pair(s)
5335
         * @return self<int|string,mixed> New map with the new key/value pairs
5336
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
5337
         * @see rekey() - Changes the keys according to the passed function
5338
         */
5339
        public function transform( \Closure $callback ) : self
5340
        {
5341
                $result = [];
32✔
5342

5343
                foreach( $this->list() as $key => $value )
32✔
5344
                {
5345
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
32✔
5346
                                $result[$newkey] = $newval;
32✔
5347
                        }
5348
                }
5349

5350
                return new static( $result );
32✔
5351
        }
5352

5353

5354
        /**
5355
         * Exchanges rows and columns for a two dimensional map.
5356
         *
5357
         * Examples:
5358
         *  Map::from( [
5359
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5360
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5361
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5362
         *  ] )->transpose();
5363
         *
5364
         *  Map::from( [
5365
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5366
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5367
         *    ['name' => 'C', 2020 => 400]
5368
         *  ] );
5369
         *
5370
         * Results:
5371
         *  [
5372
         *    'name' => ['A', 'B', 'C'],
5373
         *    2020 => [200, 300, 400],
5374
         *    2021 => [100, 200, 300],
5375
         *    2022 => [50, 100, 200]
5376
         *  ]
5377
         *
5378
         *  [
5379
         *    'name' => ['A', 'B', 'C'],
5380
         *    2020 => [200, 300, 400],
5381
         *    2021 => [100, 200],
5382
         *    2022 => [50]
5383
         *  ]
5384
         *
5385
         * @return self<int|string,mixed> New map
5386
         */
5387
        public function transpose() : self
5388
        {
5389
                $result = [];
16✔
5390

5391
                foreach( (array) $this->first( [] ) as $key => $col ) {
16✔
5392
                        $result[$key] = array_column( $this->list(), $key );
16✔
5393
                }
5394

5395
                return new static( $result );
16✔
5396
        }
5397

5398

5399
        /**
5400
         * Traverses trees of nested items passing each item to the callback.
5401
         *
5402
         * This does work for nested arrays and objects with public properties or
5403
         * objects implementing __isset() and __get() methods. To build trees
5404
         * of nested items, use the tree() method.
5405
         *
5406
         * Examples:
5407
         *   Map::from( [[
5408
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5409
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5410
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5411
         *     ]
5412
         *   ]] )->traverse();
5413
         *
5414
         *   Map::from( [[
5415
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5416
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5417
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5418
         *     ]
5419
         *   ]] )->traverse( function( $entry, $key, $level, $parent ) {
5420
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
5421
         *   } );
5422
         *
5423
         *   Map::from( [[
5424
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5425
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5426
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5427
         *     ]
5428
         *   ]] )->traverse( function( &$entry, $key, $level, $parent ) {
5429
         *     $entry['path'] = isset( $parent['path'] ) ? $parent['path'] . '/' . $entry['name'] : $entry['name'];
5430
         *     return $entry;
5431
         *   } );
5432
         *
5433
         *   Map::from( [[
5434
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
5435
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
5436
         *     ]
5437
         *   ]] )->traverse( null, 'nodes' );
5438
         *
5439
         * Results:
5440
         *   [
5441
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
5442
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5443
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
5444
         *   ]
5445
         *
5446
         *   ['- n1', '-- n2', '-- n3']
5447
         *
5448
         *   [
5449
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...], 'path' => 'n1'],
5450
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => [], 'path' => 'n1/n2'],
5451
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => [], 'path' => 'n1/n3'],
5452
         *   ]
5453
         *
5454
         *   [
5455
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
5456
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
5457
         *   ]
5458
         *
5459
         * @param \Closure|null $callback Callback with (entry, key, level, $parent) arguments, returns the entry added to result
5460
         * @param string $nestKey Key to the children of each item
5461
         * @return self<int|string,mixed> New map with all items as flat list
5462
         */
5463
        public function traverse( ?\Closure $callback = null, string $nestKey = 'children' ) : self
5464
        {
5465
                $result = [];
40✔
5466
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
40✔
5467

5468
                return map( $result );
40✔
5469
        }
5470

5471

5472
        /**
5473
         * Creates a tree structure from the list items.
5474
         *
5475
         * Use this method to rebuild trees e.g. from database records. To traverse
5476
         * trees, use the traverse() method.
5477
         *
5478
         * Examples:
5479
         *  Map::from( [
5480
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
5481
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
5482
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
5483
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
5484
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
5485
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
5486
         *  ] )->tree( 'id', 'pid' );
5487
         *
5488
         * Results:
5489
         *   [1 => [
5490
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
5491
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
5492
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
5493
         *       ]],
5494
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
5495
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
5496
         *       ]],
5497
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
5498
         *     ]
5499
         *   ]]
5500
         *
5501
         * To build the tree correctly, the items must be in order or at least the
5502
         * nodes of the lower levels must come first. For a tree like this:
5503
         * n1
5504
         * |- n2
5505
         * |  |- n3
5506
         * |- n4
5507
         * |  |- n5
5508
         * |- n6
5509
         *
5510
         * Accepted item order:
5511
         * - in order: n1, n2, n3, n4, n5, n6
5512
         * - lower levels first: n1, n2, n4, n6, n3, n5
5513
         *
5514
         * If your items are unordered, apply usort() first to the map entries, e.g.
5515
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
5516
         *     return $item1['lvl'] <=> $item2['lvl'];
5517
         *   } );
5518
         *
5519
         * @param string $idKey Name of the key with the unique ID of the node
5520
         * @param string $parentKey Name of the key with the ID of the parent node
5521
         * @param string $nestKey Name of the key with will contain the children of the node
5522
         * @return self<int|string,mixed> New map with one or more root tree nodes
5523
         */
5524
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
5525
        {
5526
                $this->list();
8✔
5527
                $trees = $refs = [];
8✔
5528

5529
                foreach( $this->list as &$node )
8✔
5530
                {
5531
                        $node[$nestKey] = [];
8✔
5532
                        $refs[$node[$idKey]] = &$node;
8✔
5533

5534
                        if( $node[$parentKey] ) {
8✔
5535
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
8✔
5536
                        } else {
5537
                                $trees[$node[$idKey]] = &$node;
8✔
5538
                        }
5539
                }
5540

5541
                return map( $trees );
8✔
5542
        }
5543

5544

5545
        /**
5546
         * Removes the passed characters from the left/right of all strings.
5547
         *
5548
         * Examples:
5549
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5550
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5551
         *
5552
         * Results:
5553
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5554
         *
5555
         * @param string $chars List of characters to trim
5556
         * @return self<int|string,mixed> Updated map for fluid interface
5557
         */
5558
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5559
        {
5560
                foreach( $this->list() as &$entry )
8✔
5561
                {
5562
                        if( is_string( $entry ) ) {
8✔
5563
                                $entry = trim( $entry, $chars );
8✔
5564
                        }
5565
                }
5566

5567
                return $this;
8✔
5568
        }
5569

5570

5571
        /**
5572
         * Sorts all elements using a callback and maintains the key association.
5573
         *
5574
         * The given callback will be used to compare the values. The callback must accept
5575
         * two parameters (item A and B) and must return -1 if item A is smaller than
5576
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5577
         * method name and an anonymous function can be passed.
5578
         *
5579
         * Examples:
5580
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( 'strcasecmp' );
5581
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( function( $itemA, $itemB ) {
5582
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5583
         *  } );
5584
         *
5585
         * Results:
5586
         *  ['b' => 'a', 'a' => 'B']
5587
         *  ['b' => 'a', 'a' => 'B']
5588
         *
5589
         * The keys are preserved using this method and no new map is created.
5590
         *
5591
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5592
         * @return self<int|string,mixed> Updated map for fluid interface
5593
         */
5594
        public function uasort( callable $callback ) : self
5595
        {
5596
                uasort( $this->list(), $callback );
16✔
5597
                return $this;
16✔
5598
        }
5599

5600

5601
        /**
5602
         * Sorts all elements using a callback and maintains the key association.
5603
         *
5604
         * The given callback will be used to compare the values. The callback must accept
5605
         * two parameters (item A and B) and must return -1 if item A is smaller than
5606
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5607
         * method name and an anonymous function can be passed.
5608
         *
5609
         * Examples:
5610
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( 'strcasecmp' );
5611
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( function( $itemA, $itemB ) {
5612
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5613
         *  } );
5614
         *
5615
         * Results:
5616
         *  ['b' => 'a', 'a' => 'B']
5617
         *  ['b' => 'a', 'a' => 'B']
5618
         *
5619
         * The keys are preserved using this method and a new map is created.
5620
         *
5621
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5622
         * @return self<int|string,mixed> Updated map for fluid interface
5623
         */
5624
        public function uasorted( callable $callback ) : self
5625
        {
5626
                return ( clone $this )->uasort( $callback );
8✔
5627
        }
5628

5629

5630
        /**
5631
         * Sorts the map elements by their keys using a callback.
5632
         *
5633
         * The given callback will be used to compare the keys. The callback must accept
5634
         * two parameters (key A and B) and must return -1 if key A is smaller than
5635
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5636
         * method name and an anonymous function can be passed.
5637
         *
5638
         * Examples:
5639
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( 'strcasecmp' );
5640
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( function( $keyA, $keyB ) {
5641
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5642
         *  } );
5643
         *
5644
         * Results:
5645
         *  ['a' => 'b', 'B' => 'a']
5646
         *  ['a' => 'b', 'B' => 'a']
5647
         *
5648
         * The keys are preserved using this method and no new map is created.
5649
         *
5650
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5651
         * @return self<int|string,mixed> Updated map for fluid interface
5652
         */
5653
        public function uksort( callable $callback ) : self
5654
        {
5655
                uksort( $this->list(), $callback );
16✔
5656
                return $this;
16✔
5657
        }
5658

5659

5660
        /**
5661
         * Sorts a copy of the map elements by their keys using a callback.
5662
         *
5663
         * The given callback will be used to compare the keys. The callback must accept
5664
         * two parameters (key A and B) and must return -1 if key A is smaller than
5665
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5666
         * method name and an anonymous function can be passed.
5667
         *
5668
         * Examples:
5669
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5670
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5671
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5672
         *  } );
5673
         *
5674
         * Results:
5675
         *  ['a' => 'b', 'B' => 'a']
5676
         *  ['a' => 'b', 'B' => 'a']
5677
         *
5678
         * The keys are preserved using this method and a new map is created.
5679
         *
5680
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5681
         * @return self<int|string,mixed> Updated map for fluid interface
5682
         */
5683
        public function uksorted( callable $callback ) : self
5684
        {
5685
                return ( clone $this )->uksort( $callback );
8✔
5686
        }
5687

5688

5689
        /**
5690
         * Builds a union of the elements and the given elements without overwriting existing ones.
5691
         * Existing keys in the map will not be overwritten
5692
         *
5693
         * Examples:
5694
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
5695
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
5696
         *
5697
         * Results:
5698
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
5699
         * isn't overwritten. In the second example, the result will be a combined
5700
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
5701
         *
5702
         * If list entries should be overwritten,  please use merge() instead!
5703
         * The keys are preserved using this method and no new map is created.
5704
         *
5705
         * @param iterable<int|string,mixed> $elements List of elements
5706
         * @return self<int|string,mixed> Updated map for fluid interface
5707
         */
5708
        public function union( iterable $elements ) : self
5709
        {
5710
                $this->list = $this->list() + $this->array( $elements );
16✔
5711
                return $this;
16✔
5712
        }
5713

5714

5715
        /**
5716
         * Returns only unique elements from the map incl. their keys.
5717
         *
5718
         * Examples:
5719
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
5720
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
5721
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
5722
         *
5723
         * Results:
5724
         * [0 => 'a', 1 => 'b', 3 => 'c']
5725
         * [['p' => 1], ['p' => 2]]
5726
         * [['i' => ['p' => '1']]]
5727
         *
5728
         * Two elements are considered equal if comparing their string representions returns TRUE:
5729
         * (string) $elem1 === (string) $elem2
5730
         *
5731
         * The keys of the elements are only preserved in the new map if no key is passed.
5732
         *
5733
         * @param string|null $key Key or path of the nested array or object to check for
5734
         * @return self<int|string,mixed> New map
5735
         */
5736
        public function unique( ?string $key = null ) : self
5737
        {
5738
                if( $key !== null ) {
32✔
5739
                        return $this->col( null, $key )->values();
16✔
5740
                }
5741

5742
                return new static( array_unique( $this->list() ) );
16✔
5743
        }
5744

5745

5746
        /**
5747
         * Pushes an element onto the beginning of the map without returning a new map.
5748
         *
5749
         * Examples:
5750
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
5751
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
5752
         *
5753
         * Results:
5754
         *  ['d', 'a', 'b']
5755
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
5756
         *
5757
         * The keys of the elements are only preserved in the new map if no key is passed.
5758
         *
5759
         * Performance note:
5760
         * The bigger the list, the higher the performance impact because unshift()
5761
         * needs to create a new list and copies all existing elements to the new
5762
         * array. Usually, it's better to push() new entries at the end and reverse()
5763
         * the list afterwards:
5764
         *
5765
         *  $map->push( 'a' )->push( 'b' )->reverse();
5766
         * instead of
5767
         *  $map->unshift( 'a' )->unshift( 'b' );
5768
         *
5769
         * @param mixed $value Item to add at the beginning
5770
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
5771
         * @return self<int|string,mixed> Updated map for fluid interface
5772
         */
5773
        public function unshift( $value, $key = null ) : self
5774
        {
5775
                if( $key === null ) {
24✔
5776
                        array_unshift( $this->list(), $value );
16✔
5777
                } else {
5778
                        $this->list = [$key => $value] + $this->list();
8✔
5779
                }
5780

5781
                return $this;
24✔
5782
        }
5783

5784

5785
        /**
5786
         * Sorts all elements using a callback using new keys.
5787
         *
5788
         * The given callback will be used to compare the values. The callback must accept
5789
         * two parameters (item A and B) and must return -1 if item A is smaller than
5790
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5791
         * method name and an anonymous function can be passed.
5792
         *
5793
         * Examples:
5794
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( 'strcasecmp' );
5795
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( function( $itemA, $itemB ) {
5796
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5797
         *  } );
5798
         *
5799
         * Results:
5800
         *  [0 => 'a', 1 => 'B']
5801
         *  [0 => 'a', 1 => 'B']
5802
         *
5803
         * The keys aren't preserved and elements get a new index. No new map is created.
5804
         *
5805
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5806
         * @return self<int|string,mixed> Updated map for fluid interface
5807
         */
5808
        public function usort( callable $callback ) : self
5809
        {
5810
                usort( $this->list(), $callback );
16✔
5811
                return $this;
16✔
5812
        }
5813

5814

5815
        /**
5816
         * Sorts a copy of all elements using a callback using new keys.
5817
         *
5818
         * The given callback will be used to compare the values. The callback must accept
5819
         * two parameters (item A and B) and must return -1 if item A is smaller than
5820
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5821
         * method name and an anonymous function can be passed.
5822
         *
5823
         * Examples:
5824
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
5825
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
5826
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5827
         *  } );
5828
         *
5829
         * Results:
5830
         *  [0 => 'a', 1 => 'B']
5831
         *  [0 => 'a', 1 => 'B']
5832
         *
5833
         * The keys aren't preserved, elements get a new index and a new map is created.
5834
         *
5835
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5836
         * @return self<int|string,mixed> Updated map for fluid interface
5837
         */
5838
        public function usorted( callable $callback ) : self
5839
        {
5840
                return ( clone $this )->usort( $callback );
8✔
5841
        }
5842

5843

5844
        /**
5845
         * Resets the keys and return the values in a new map.
5846
         *
5847
         * Examples:
5848
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
5849
         *
5850
         * Results:
5851
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
5852
         *
5853
         * @return self<int|string,mixed> New map of the values
5854
         */
5855
        public function values() : self
5856
        {
5857
                return new static( array_values( $this->list() ) );
96✔
5858
        }
5859

5860

5861
        /**
5862
         * Applies the given callback to all elements.
5863
         *
5864
         * To change the values of the Map, specify the value parameter as reference
5865
         * (&$value). You can only change the values but not the keys nor the array
5866
         * structure.
5867
         *
5868
         * Examples:
5869
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
5870
         *    $value = strtoupper( $value );
5871
         *  } );
5872
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
5873
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
5874
         *  } );
5875
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
5876
         *    $value = $data[$value] ?? $value;
5877
         *  }, [1 => 'one', 2 => 'two'] );
5878
         *
5879
         * Results:
5880
         * The first example will change the Map elements to:
5881
         *   ['A', 'B', ['C', 'D'], 'E']
5882
         * The output of the second one will be:
5883
         *  ASCII 66 is B
5884
         *  ASCII 97 is a
5885
         * The last example changes the Map elements to:
5886
         *  ['one', 'two', 3]
5887
         *
5888
         * By default, Map elements which are arrays will be traversed recursively.
5889
         * To iterate over the Map elements only, pass FALSE as third parameter.
5890
         *
5891
         * @param callable $callback Function with (item, key, data) parameters
5892
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
5893
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
5894
         * @return self<int|string,mixed> Updated map for fluid interface
5895
         */
5896
        public function walk( callable $callback, $data = null, bool $recursive = true ) : self
5897
        {
5898
                if( $recursive ) {
24✔
5899
                        array_walk_recursive( $this->list(), $callback, $data );
16✔
5900
                } else {
5901
                        array_walk( $this->list(), $callback, $data );
8✔
5902
                }
5903

5904
                return $this;
24✔
5905
        }
5906

5907

5908
        /**
5909
         * Filters the list of elements by a given condition.
5910
         *
5911
         * Examples:
5912
         *  Map::from( [
5913
         *    ['id' => 1, 'type' => 'name'],
5914
         *    ['id' => 2, 'type' => 'short'],
5915
         *  ] )->where( 'type', '==', 'name' );
5916
         *
5917
         *  Map::from( [
5918
         *    ['id' => 3, 'price' => 10],
5919
         *    ['id' => 4, 'price' => 50],
5920
         *  ] )->where( 'price', '>', 20 );
5921
         *
5922
         *  Map::from( [
5923
         *    ['id' => 3, 'price' => 10],
5924
         *    ['id' => 4, 'price' => 50],
5925
         *  ] )->where( 'price', 'in', [10, 25] );
5926
         *
5927
         *  Map::from( [
5928
         *    ['id' => 3, 'price' => 10],
5929
         *    ['id' => 4, 'price' => 50],
5930
         *  ] )->where( 'price', '-', [10, 100] );
5931
         *
5932
         *  Map::from( [
5933
         *    ['item' => ['id' => 3, 'price' => 10]],
5934
         *    ['item' => ['id' => 4, 'price' => 50]],
5935
         *  ] )->where( 'item/price', '>', 30 );
5936
         *
5937
         * Results:
5938
         *  [0 => ['id' => 1, 'type' => 'name']]
5939
         *  [1 => ['id' => 4, 'price' => 50]]
5940
         *  [0 => ['id' => 3, 'price' => 10]]
5941
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
5942
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
5943
         *
5944
         * Available operators are:
5945
         * * '==' : Equal
5946
         * * '===' : Equal and same type
5947
         * * '!=' : Not equal
5948
         * * '!==' : Not equal and same type
5949
         * * '<=' : Smaller than an equal
5950
         * * '>=' : Greater than an equal
5951
         * * '<' : Smaller
5952
         * * '>' : Greater
5953
         * 'in' : Array of value which are in the list of values
5954
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
5955
         *
5956
         * This does also work for multi-dimensional arrays by passing the keys
5957
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5958
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5959
         * public properties of objects or objects implementing __isset() and __get() methods.
5960
         *
5961
         * The keys of the original map are preserved in the returned map.
5962
         *
5963
         * @param string $key Key or path of the value in the array or object used for comparison
5964
         * @param string $op Operator used for comparison
5965
         * @param mixed $value Value used for comparison
5966
         * @return self<int|string,mixed> New map for fluid interface
5967
         */
5968
        public function where( string $key, string $op, $value ) : self
5969
        {
5970
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
36✔
5971

5972
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
48✔
5973
                        {
5974
                                switch( $op )
5✔
5975
                                {
5976
                                        case '-':
40✔
5977
                                                $list = (array) $value;
8✔
5978
                                                return $val >= current( $list ) && $val <= end( $list );
8✔
5979
                                        case 'in': return in_array( $val, (array) $value );
32✔
5980
                                        case '<': return $val < $value;
24✔
5981
                                        case '>': return $val > $value;
24✔
5982
                                        case '<=': return $val <= $value;
16✔
5983
                                        case '>=': return $val >= $value;
16✔
5984
                                        case '===': return $val === $value;
16✔
5985
                                        case '!==': return $val !== $value;
16✔
5986
                                        case '!=': return $val != $value;
16✔
5987
                                        default: return $val == $value;
16✔
5988
                                }
5989
                        }
5990

5991
                        return false;
8✔
5992
                } );
48✔
5993
        }
5994

5995

5996
        /**
5997
         * Returns a copy of the map with the element at the given index replaced with the given value.
5998
         *
5999
         * Examples:
6000
         *  $m = Map::from( ['a' => 1] );
6001
         *  $m->with( 2, 'b' );
6002
         *  $m->with( 'a', 2 );
6003
         *
6004
         * Results:
6005
         *  ['a' => 1, 2 => 'b']
6006
         *  ['a' => 2]
6007
         *
6008
         * The original map ($m) stays untouched!
6009
         * This method is a shortcut for calling the copy() and set() methods.
6010
         *
6011
         * @param int|string $key Array key to set or replace
6012
         * @param mixed $value New value for the given key
6013
         * @return self<int|string,mixed> New map
6014
         */
6015
        public function with( $key, $value ) : self
6016
        {
6017
                return ( clone $this )->set( $key, $value );
8✔
6018
        }
6019

6020

6021
        /**
6022
         * Merges the values of all arrays at the corresponding index.
6023
         *
6024
         * Examples:
6025
         *  $en = ['one', 'two', 'three'];
6026
         *  $es = ['uno', 'dos', 'tres'];
6027
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
6028
         *
6029
         * Results:
6030
         *  [
6031
         *    [1, 'one', 'uno'],
6032
         *    [2, 'two', 'dos'],
6033
         *    [3, 'three', 'tres'],
6034
         *  ]
6035
         *
6036
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
6037
         * @return self<int|string,mixed> New map of arrays
6038
         */
6039
        public function zip( ...$arrays ) : self
6040
        {
6041
                $args = array_map( function( $items ) {
6✔
6042
                        return $this->array( $items );
8✔
6043
                }, $arrays );
8✔
6044

6045
                return new static( array_map( null, $this->list(), ...$args ) );
8✔
6046
        }
6047

6048

6049
        /**
6050
         * Returns a plain array of the given elements.
6051
         *
6052
         * @param mixed $elements List of elements or single value
6053
         * @return array<int|string,mixed> Plain array
6054
         */
6055
        protected function array( $elements ) : array
6056
        {
6057
                if( is_array( $elements ) ) {
2,048✔
6058
                        return $elements;
1,968✔
6059
                }
6060

6061
                if( $elements instanceof \Closure ) {
312✔
6062
                        return (array) $elements();
×
6063
                }
6064

6065
                if( $elements instanceof \Aimeos\Map ) {
312✔
6066
                        return $elements->toArray();
184✔
6067
                }
6068

6069
                if( is_iterable( $elements ) ) {
136✔
6070
                        return iterator_to_array( $elements, true );
24✔
6071
                }
6072

6073
                return $elements !== null ? [$elements] : [];
112✔
6074
        }
6075

6076

6077
        /**
6078
         * Flattens a multi-dimensional array or map into a single level array.
6079
         *
6080
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6081
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
6082
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6083
         */
6084
        protected function flatten( iterable $entries, array &$result, int $depth ) : void
6085
        {
6086
                foreach( $entries as $entry )
40✔
6087
                {
6088
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
6089
                                $this->flatten( $entry, $result, $depth - 1 );
32✔
6090
                        } else {
6091
                                $result[] = $entry;
40✔
6092
                        }
6093
                }
6094
        }
10✔
6095

6096

6097
        /**
6098
         * Flattens a multi-dimensional array or map into a single level array.
6099
         *
6100
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6101
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
6102
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6103
         */
6104
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
6105
        {
6106
                foreach( $entries as $key => $entry )
40✔
6107
                {
6108
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
6109
                                $this->kflatten( $entry, $result, $depth - 1 );
40✔
6110
                        } else {
6111
                                $result[$key] = $entry;
40✔
6112
                        }
6113
                }
6114
        }
10✔
6115

6116

6117
        /**
6118
         * Returns a reference to the array of elements
6119
         *
6120
         * @return array Reference to the array of elements
6121
         */
6122
        protected function &list() : array
6123
        {
6124
                if( !is_array( $this->list ) ) {
2,784✔
6125
                        $this->list = $this->array( $this->list );
×
6126
                }
6127

6128
                return $this->list;
2,784✔
6129
        }
6130

6131

6132
        /**
6133
         * Returns a configuration value from an array.
6134
         *
6135
         * @param array<mixed>|object $entry The array or object to look at
6136
         * @param array<string> $parts Path parts to look for inside the array or object
6137
         * @return mixed Found value or null if no value is available
6138
         */
6139
        protected function val( $entry, array $parts )
6140
        {
6141
                foreach( $parts as $part )
312✔
6142
                {
6143
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
312✔
6144
                                $entry = $entry[$part];
176✔
6145
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
200✔
6146
                                $entry = $entry->{$part};
8✔
6147
                        } else {
6148
                                return null;
207✔
6149
                        }
6150
                }
6151

6152
                return $entry;
176✔
6153
        }
6154

6155

6156
        /**
6157
         * Visits each entry, calls the callback and returns the items in the result argument
6158
         *
6159
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
6160
         * @param array<mixed> $result Numerically indexed list of all visited entries
6161
         * @param int $level Current depth of the nodes in the tree
6162
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
6163
         * @param string $nestKey Key to the children of each entry
6164
         * @param array<mixed>|object|null $parent Parent entry
6165
         */
6166
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, $parent = null ) : void
6167
        {
6168
                foreach( $entries as $key => $entry )
40✔
6169
                {
6170
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
40✔
6171

6172
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
40✔
6173
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
32✔
6174
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
8✔
6175
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
12✔
6176
                        }
6177
                }
6178
        }
10✔
6179
}
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