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

aimeos / map / e5f1b1e2-8f48-4167-9577-6efab262f169

01 Mar 2026 05:03PM UTC coverage: 96.336% (-1.5%) from 97.847%
e5f1b1e2-8f48-4167-9577-6efab262f169

push

circleci

aimeos
Fixed generating coverage information

815 of 846 relevant lines covered (96.34%)

6.06 hits per line

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

96.32
/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
 * @phpstan-consistent-constructor
19
 */
20
class Map implements \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable
21
{
22
        /**
23
         * @var array<string,\Closure>
24
         */
25
        protected static array $methods = [];
26

27
        /**
28
         * @var non-empty-string
29
         */
30
        protected static string $delim = '/';
31

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

37
        /**
38
         * @var non-empty-string
39
         */
40
        protected string $sep = '/';
41

42

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

57

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

79
                return call_user_func_array( \Closure::bind( static::$methods[$name], null, static::class ), $params );
1✔
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 ) : mixed
113
        {
114
                if( isset( static::$methods[$name] ) ) {
4✔
115
                        return call_user_func_array( static::$methods[$name]->bindTo( $this, static::class ), $params );
2✔
116
                }
117

118
                $result = [];
2✔
119

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

127
                return new static( $result );
2✔
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 );
1✔
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;
1✔
163

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

168
                return $old;
1✔
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 !== '' ) {
8✔
207
                        return new static( explode( $delimiter, $string, $limit ) );
4✔
208
                }
209

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

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

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

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

225
                return new static( $parts );
2✔
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, mixed $value, int $start = 0 ) : self
248
        {
249
                return new static( array_fill( $start, $num, $value ) );
2✔
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( mixed $elements = [] ) : self
272
        {
273
                if( $elements instanceof self ) {
202✔
274
                        return $elements;
3✔
275
                }
276

277
                return new static( $elements );
202✔
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 ) {
4✔
312
                        return new static( $result );
3✔
313
                }
314

315
                throw new \RuntimeException( 'Not a valid JSON string: ' . $json );
1✔
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 ) {
3✔
342
                        self::$methods[$method] = $fcn;
3✔
343
                }
344

345
                return self::$methods[$method] ?? null;
3✔
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 = [];
3✔
380

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

386
                return new static( $list );
3✔
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( \Closure|int|string $value ) : self
413
        {
414
                if( ( $pos = $this->pos( $value ) ) === null ) {
4✔
415
                        return new static();
1✔
416
                }
417

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

421

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

432

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

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

463
                return false;
×
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 );
4✔
498
                return $this;
4✔
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 );
1✔
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 );
4✔
567
                return $this;
4✔
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 );
1✔
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 ) : mixed
626
        {
627
                $pair = array_slice( $this->list(), $pos, 1 );
1✔
628
                return !empty( $pair ) ? current( $pair ) : null;
1✔
629
        }
630

631

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

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

666

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

694

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

736
                if( $default instanceof \Closure ) {
1✔
737
                        $default = $default( $val );
×
738
                }
739

740
                if( $default instanceof \Throwable ) {
1✔
741
                        throw $default;
×
742
                }
743

744
                return (bool) $default;
1✔
745
        }
746

747

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

774
                foreach( $this->list() as $key => $item )
1✔
775
                {
776
                        if( is_object( $item ) ) {
1✔
777
                                $result[$key] = $item->{$name}( ...$params );
1✔
778
                        }
779
                }
780

781
                return new static( $result );
1✔
782
        }
783

784

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

825
                return $this;
1✔
826
        }
827

828

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

856
                return new static( array_chunk( $this->list(), $size, $preserve ) );
2✔
857
        }
858

859

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

871

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

892
                foreach( $this->list() as $key => $item ) {
1✔
893
                        $list[$key] = is_object( $item ) ? clone $item : $item;
1✔
894
                }
895

896
                return new static( $list );
1✔
897
        }
898

899

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

939
                if( ( $valuecol === null || count( $vparts ) === 1 )
9✔
940
                        && ( $indexcol === null || count( $iparts ) === 1 )
9✔
941
                ) {
942
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
6✔
943
                }
944

945
                $list = [];
3✔
946

947
                foreach( $this->list() as $key => $item )
3✔
948
                {
949
                        $v = $valuecol ? $this->val( $item, $vparts ) : $item;
3✔
950

951
                        if( $indexcol && ( $k = (string) $this->val( $item, $iparts ) ) ) {
3✔
952
                                $list[$k] = $v;
2✔
953
                        } else {
954
                                $list[$key] = $v;
1✔
955
                        }
956
                }
957

958
                return new static( $list );
3✔
959
        }
960

961

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

996
                $result = [];
5✔
997
                $this->kflatten( $this->list(), $result, $depth ?? 0x7fffffff );
5✔
998
                return new static( $result );
5✔
999
        }
1000

1001

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

1019

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

1035

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

1054
                foreach( $elements as $item ) {
2✔
1055
                        $this->list[] = $item;
2✔
1056
                }
1057

1058
                return $this;
2✔
1059
        }
1060

1061

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

1095
                if( !is_string( $key ) ) {
1✔
1096
                        throw new \InvalidArgumentException( 'Key must be a string' );
×
1097
                }
1098

1099
                if( $value === null ) {
1✔
1100
                        return !$this->where( $key, '==', $operator )->isEmpty();
1✔
1101
                }
1102

1103
                return !$this->where( $key, $operator, $value )->isEmpty();
1✔
1104
        }
1105

1106

1107
        /**
1108
         * Creates a new map with the same elements.
1109
         *
1110
         * Both maps share the same array until one of the map objects modifies the
1111
         * array. Then, the array is copied and the copy is modfied (copy on write).
1112
         *
1113
         * @return self<int|string,mixed> New map
1114
         */
1115
        public function copy() : self
1116
        {
1117
                return clone $this;
3✔
1118
        }
1119

1120

1121
        /**
1122
         * Counts the total number of elements in the map.
1123
         *
1124
         * @return int Number of elements
1125
         */
1126
        public function count() : int
1127
        {
1128
                return count( $this->list() );
17✔
1129
        }
1130

1131

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

1168
                        $col = function( $item ) use ( $parts ) {
3✔
1169
                                return (string) $this->val( $item, $parts );
3✔
1170
                        };
3✔
1171
                }
1172

1173
                return new static( array_count_values( array_map( $col, $this->list() ) ) );
4✔
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 ) {
3✔
1235
                        return new static( array_udiff( $this->list(), $this->array( $elements ), $callback ) );
1✔
1236
                }
1237

1238
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
3✔
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 ) {
2✔
1276
                        return new static( array_diff_uassoc( $this->list(), $this->array( $elements ), $callback ) );
1✔
1277
                }
1278

1279
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
2✔
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 ) {
2✔
1316
                        return new static( array_diff_ukey( $this->list(), $this->array( $elements ), $callback ) );
1✔
1317
                }
1318

1319
                return new static( array_diff_key( $this->list(), $this->array( $elements ) ) );
2✔
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() );
1✔
1351
                return $this;
1✔
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
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( fn( $item, $key ) => $item['i']['p'] )
1366
         *
1367
         * Results:
1368
         *  [2 => '1']
1369
         *  [1 => ['p' => 1]]
1370
         *  [1 => ['i' => ['p' => 1]]]
1371
         *  [1 => ['i' => ['p' => 1]]]
1372
         *
1373
         * This does also work for multi-dimensional arrays by passing the keys
1374
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1375
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1376
         * public properties of objects or objects implementing __isset() and __get() methods.
1377
         *
1378
         * The keys are preserved using this method.
1379
         *
1380
         * @param \Closure|string|null $col Key, path of the nested array or anonymous function with ($item, $key) parameters returning the value for comparison
1381
         * @return self<int|string,mixed> New map
1382
         */
1383
        public function duplicates( \Closure|string|null $col = null ) : self
1384
        {
1385
                $list = $map = $this->list();
4✔
1386

1387
                if( $col !== null ) {
4✔
1388
                        $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
3✔
1389
                }
1390

1391
                return new static( array_diff_key( $list, array_unique( $map ) ) );
4✔
1392
        }
1393

1394

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

1420
                return $this;
2✔
1421
        }
1422

1423

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

1443

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

1469
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
6✔
1470
        }
1471

1472

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

1500
                return true;
1✔
1501
        }
1502

1503

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

1525

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

1555
                return new static( array_filter( $this->list() ) );
1✔
1556
        }
1557

1558

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

1589
                if( !empty( $list ) )
5✔
1590
                {
1591
                        if( $reverse )
5✔
1592
                        {
1593
                                $value = end( $list );
2✔
1594
                                $key = key( $list );
2✔
1595

1596
                                do
1597
                                {
1598
                                        if( $callback( $value, $key ) ) {
2✔
1599
                                                return $value;
1✔
1600
                                        }
1601
                                }
1602
                                // @phpstan-ignore-next-line notIdentical.alwaysTrue
1603
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
2✔
1604

1605
                                reset( $list );
1✔
1606
                        }
1607
                        else
1608
                        {
1609
                                foreach( $list as $key => $value )
3✔
1610
                                {
1611
                                        if( $callback( $value, $key ) ) {
3✔
1612
                                                return $value;
1✔
1613
                                        }
1614
                                }
1615
                        }
1616
                }
1617

1618
                if( $default instanceof \Closure ) {
3✔
1619
                        return $default();
1✔
1620
                }
1621

1622
                if( $default instanceof \Throwable ) {
2✔
1623
                        throw $default;
1✔
1624
                }
1625

1626
                return $default;
1✔
1627
        }
1628

1629

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

1663
                if( !empty( $list ) )
5✔
1664
                {
1665
                        if( $reverse )
2✔
1666
                        {
1667
                                $value = end( $list );
1✔
1668
                                $key = key( $list );
1✔
1669

1670
                                do
1671
                                {
1672
                                        if( $callback( $value, $key ) ) {
1✔
1673
                                                return $key;
1✔
1674
                                        }
1675
                                }
1676
                                // @phpstan-ignore-next-line notIdentical.alwaysTrue
1677
                                while( ( $value = prev( $list ) ) !== false && ( $key = key( $list ) ) !== null );
×
1678

1679
                                reset( $list );
×
1680
                        }
1681
                        else
1682
                        {
1683
                                foreach( $list as $key => $value )
1✔
1684
                                {
1685
                                        if( $callback( $value, $key ) ) {
1✔
1686
                                                return $key;
1✔
1687
                                        }
1688
                                }
1689
                        }
1690
                }
1691

1692
                if( $default instanceof \Closure ) {
3✔
1693
                        return $default();
1✔
1694
                }
1695

1696
                if( $default instanceof \Throwable ) {
2✔
1697
                        throw $default;
1✔
1698
                }
1699

1700
                return $default;
1✔
1701
        }
1702

1703

1704
        /**
1705
         * Returns the first element from the map.
1706
         *
1707
         * Examples:
1708
         *  Map::from( ['a', 'b'] )->first();
1709
         *  Map::from( [] )->first( 'x' );
1710
         *  Map::from( [] )->first( new \Exception( 'error' ) );
1711
         *  Map::from( [] )->first( function() { return rand(); } );
1712
         *
1713
         * Results:
1714
         * The first example will return 'b' and the second one 'x'. The third example
1715
         * will throw the exception passed if the map contains no elements. In the
1716
         * fourth example, a random value generated by the closure function will be
1717
         * returned.
1718
         *
1719
         * Using this method doesn't affect the internal array pointer.
1720
         *
1721
         * @param mixed $default Default value, closure or exception if the map contains no elements
1722
         * @return mixed First value of map, (generated) default value or an exception
1723
         */
1724
        public function first( mixed $default = null ) : mixed
1725
        {
1726
                if( !empty( $this->list() ) ) {
11✔
1727
                        return current( array_slice( $this->list(), 0, 1 ) );
6✔
1728
                }
1729

1730
                if( $default instanceof \Closure ) {
6✔
1731
                        return $default();
1✔
1732
                }
1733

1734
                if( $default instanceof \Throwable ) {
5✔
1735
                        throw $default;
2✔
1736
                }
1737

1738
                return $default;
3✔
1739
        }
1740

1741

1742
        /**
1743
         * Returns the first key from the map.
1744
         *
1745
         * Examples:
1746
         *  Map::from( ['a' => 1, 'b' => 2] )->firstKey();
1747
         *  Map::from( [] )->firstKey( 'x' );
1748
         *  Map::from( [] )->firstKey( new \Exception( 'error' ) );
1749
         *  Map::from( [] )->firstKey( function() { return rand(); } );
1750
         *
1751
         * Results:
1752
         * The first example will return 'a' and the second one 'x', the third one will throw
1753
         * an exception and the last one will return a random value.
1754
         *
1755
         * Using this method doesn't affect the internal array pointer.
1756
         *
1757
         * @param mixed $default Default value, closure or exception if the map contains no elements
1758
         * @return mixed First key of map, (generated) default value or an exception
1759
         */
1760
        public function firstKey( mixed $default = null ) : mixed
1761
        {
1762
                $list = $this->list();
5✔
1763

1764
                // PHP 7.x compatibility
1765
                if( function_exists( 'array_key_first' ) ) {
5✔
1766
                        $key = array_key_first( $list );
5✔
1767
                } else {
1768
                        $key = key( array_slice( $list, 0, 1, true ) );
×
1769
                }
1770

1771
                if( $key !== null ) {
5✔
1772
                        return $key;
1✔
1773
                }
1774

1775
                if( $default instanceof \Closure ) {
4✔
1776
                        return $default();
1✔
1777
                }
1778

1779
                if( $default instanceof \Throwable ) {
3✔
1780
                        throw $default;
1✔
1781
                }
1782

1783
                return $default;
2✔
1784
        }
1785

1786

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

1820
                $result = [];
5✔
1821
                $this->nflatten( $this->list(), $result, $depth ?? 0x7fffffff );
5✔
1822
                return new static( $result );
5✔
1823
        }
1824

1825

1826
        /**
1827
         * Creates a new map with keys joined recursively.
1828
         *
1829
         * Examples:
1830
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->flatten();
1831
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->flatten( 1 );
1832
         *  Map::from( ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]] )->sep( '.' )->flatten();
1833
         *
1834
         * Results:
1835
         *  ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3]
1836
         *  ['a/b' => ['c' => 1, 'd' => 2], 'b/e' => 3]
1837
         *  ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3]
1838
         *
1839
         * To create the original multi-dimensional array again, use the unflatten() method.
1840
         *
1841
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1842
         * @return self New map with keys joined recursively, up to the specified depth
1843
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1844
         */
1845
        public function flatten( ?int $depth = null ) : self
1846
        {
1847
                if( $depth < 0 ) {
2✔
1848
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
1✔
1849
                }
1850

1851
                $result = [];
1✔
1852
                $this->rflatten( $this->list(), $result, $depth ?? 0x7fffffff );
1✔
1853

1854
                return new static( $result );
1✔
1855
        }
1856

1857

1858
        /**
1859
         * Exchanges the keys with their values and vice versa.
1860
         *
1861
         * Examples:
1862
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->flip();
1863
         *
1864
         * Results:
1865
         *  ['X' => 'a', 'Y' => 'b']
1866
         *
1867
         * @return self<int|string,mixed> New map with keys as values and values as keys
1868
         */
1869
        public function flip() : self
1870
        {
1871
                return new static( array_flip( $this->list() ) );
1✔
1872
        }
1873

1874

1875
        /**
1876
         * Returns an element by key and casts it to float if possible.
1877
         *
1878
         * Examples:
1879
         *  Map::from( ['a' => true] )->float( 'a' );
1880
         *  Map::from( ['a' => 1] )->float( 'a' );
1881
         *  Map::from( ['a' => '1.1'] )->float( 'a' );
1882
         *  Map::from( ['a' => '10'] )->float( 'a' );
1883
         *  Map::from( ['a' => ['b' => ['c' => 1.1]]] )->float( 'a/b/c' );
1884
         *  Map::from( [] )->float( 'c', function( $val ) { return 1.1; } );
1885
         *  Map::from( [] )->float( 'a', 1.1 );
1886
         *
1887
         *  Map::from( [] )->float( 'b' );
1888
         *  Map::from( ['b' => ''] )->float( 'b' );
1889
         *  Map::from( ['b' => null] )->float( 'b' );
1890
         *  Map::from( ['b' => 'abc'] )->float( 'b' );
1891
         *  Map::from( ['b' => [1]] )->float( 'b' );
1892
         *  Map::from( ['b' => #resource] )->float( 'b' );
1893
         *  Map::from( ['b' => new \stdClass] )->float( 'b' );
1894
         *
1895
         *  Map::from( [] )->float( 'c', new \Exception( 'error' ) );
1896
         *
1897
         * Results:
1898
         * The first eight examples will return the float values for the passed keys
1899
         * while the 9th to 14th example returns 0. The last example will throw an exception.
1900
         *
1901
         * This does also work for multi-dimensional arrays by passing the keys
1902
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1903
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1904
         * public properties of objects or objects implementing __isset() and __get() methods.
1905
         *
1906
         * @param int|string $key Key or path to the requested item
1907
         * @param \Closure|\Throwable|float $default Default value if key isn't found
1908
         * @return float Value from map or default value
1909
         */
1910
        public function float( int|string $key, \Closure|\Throwable|float $default = 0.0 ) : float
1911
        {
1912
                if( is_scalar( $val = $this->get( $key, $default ) ) ) {
3✔
1913
                        return (float) $val;
2✔
1914
                }
1915

1916
                if( $default instanceof \Closure ) {
1✔
1917
                        return (float) $default( $val );
×
1918
                }
1919

1920
                if( $default instanceof \Throwable ) {
1✔
1921
                        throw $default;
×
1922
                }
1923

1924
                return (float) $default;
1✔
1925
        }
1926

1927

1928
        /**
1929
         * Returns an element from the map by key.
1930
         *
1931
         * Examples:
1932
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'a' );
1933
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'c', 'Z' );
1934
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->get( 'a/b/c' );
1935
         *  Map::from( [] )->get( 'Y', new \Exception( 'error' ) );
1936
         *  Map::from( [] )->get( 'Y', function() { return rand(); } );
1937
         *
1938
         * Results:
1939
         * The first example will return 'X', the second 'Z' and the third 'Y'. The forth
1940
         * example will throw the exception passed if the map contains no elements. In
1941
         * the fifth example, a random value generated by the closure function will be
1942
         * returned.
1943
         *
1944
         * This does also work for multi-dimensional arrays by passing the keys
1945
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1946
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1947
         * public properties of objects or objects implementing __isset() and __get() methods.
1948
         *
1949
         * @param int|string $key Key or path to the requested item
1950
         * @param mixed $default Default value if no element matches
1951
         * @return mixed Value from map or default value
1952
         */
1953
        public function get( int|string $key, mixed $default = null ) : mixed
1954
        {
1955
                $list = $this->list();
23✔
1956

1957
                if( array_key_exists( $key, $list ) ) {
23✔
1958
                        return $list[$key];
6✔
1959
                }
1960

1961
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
21✔
1962
                        return $v;
7✔
1963
                }
1964

1965
                if( $default instanceof \Closure ) {
18✔
1966
                        return $default();
6✔
1967
                }
1968

1969
                if( $default instanceof \Throwable ) {
12✔
1970
                        throw $default;
6✔
1971
                }
1972

1973
                return $default;
6✔
1974
        }
1975

1976

1977
        /**
1978
         * Returns an iterator for the elements.
1979
         *
1980
         * This method will be used by e.g. foreach() to loop over all entries:
1981
         *  foreach( Map::from( ['a', 'b'] ) as $value )
1982
         *
1983
         * @return \ArrayIterator<int|string,mixed> Iterator for map elements
1984
         */
1985
        public function getIterator() : \ArrayIterator
1986
        {
1987
                return new \ArrayIterator( $this->list() );
5✔
1988
        }
1989

1990

1991
        /**
1992
         * Returns only items which matches the regular expression.
1993
         *
1994
         * All items are converted to string first before they are compared to the
1995
         * regular expression. Thus, fractions of ".0" will be removed in float numbers
1996
         * which may result in unexpected results.
1997
         *
1998
         * Examples:
1999
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/b/' );
2000
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/a/', PREG_GREP_INVERT );
2001
         *  Map::from( [1.5, 0, 1.0, 'a'] )->grep( '/^(\d+)?\.\d+$/' );
2002
         *
2003
         * Results:
2004
         *  ['ab', 'bc']
2005
         *  ['bc', 'cd']
2006
         *  [1.5] // float 1.0 is converted to string "1"
2007
         *
2008
         * The keys are preserved using this method.
2009
         *
2010
         * @param string $pattern Regular expression pattern, e.g. "/ab/"
2011
         * @param int $flags PREG_GREP_INVERT to return elements not matching the pattern
2012
         * @return self<int|string,mixed> New map containing only the matched elements
2013
         */
2014
        public function grep( string $pattern, int $flags = 0 ) : self
2015
        {
2016
                if( ( $result = preg_grep( $pattern, $this->list(), $flags ) ) === false )
4✔
2017
                {
2018
                        switch( preg_last_error() )
1✔
2019
                        {
2020
                                case PREG_INTERNAL_ERROR: $msg = 'Internal error'; break;
1✔
2021
                                case PREG_BACKTRACK_LIMIT_ERROR: $msg = 'Backtrack limit error'; break;
×
2022
                                case PREG_RECURSION_LIMIT_ERROR: $msg = 'Recursion limit error'; break;
×
2023
                                case PREG_BAD_UTF8_ERROR: $msg = 'Bad UTF8 error'; break;
×
2024
                                case PREG_BAD_UTF8_OFFSET_ERROR: $msg = 'Bad UTF8 offset error'; break;
×
2025
                                case PREG_JIT_STACKLIMIT_ERROR: $msg = 'JIT stack limit error'; break;
×
2026
                                default: $msg = 'Unknown error';
×
2027
                        }
2028

2029
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
1✔
2030
                }
2031

2032
                return new static( $result );
3✔
2033
        }
2034

2035

2036
        /**
2037
         * Groups associative array elements or objects by the passed key or closure.
2038
         *
2039
         * Instead of overwriting items with the same keys like to the col() method
2040
         * does, groupBy() keeps all entries in sub-arrays. It's preserves the keys
2041
         * of the orignal map entries too.
2042
         *
2043
         * Examples:
2044
         *  $list = [
2045
         *    10 => ['aid' => 123, 'code' => 'x-abc'],
2046
         *    20 => ['aid' => 123, 'code' => 'x-def'],
2047
         *    30 => ['aid' => 456, 'code' => 'x-def']
2048
         *  ];
2049
         *  Map::from( $list )->groupBy( 'aid' );
2050
         *  Map::from( $list )->groupBy( function( $item, $key ) {
2051
         *    return substr( $item['code'], -3 );
2052
         *  } );
2053
         *  Map::from( $list )->groupBy( 'xid' );
2054
         *
2055
         * Results:
2056
         *  [
2057
         *    123 => [10 => ['aid' => 123, 'code' => 'x-abc'], 20 => ['aid' => 123, 'code' => 'x-def']],
2058
         *    456 => [30 => ['aid' => 456, 'code' => 'x-def']]
2059
         *  ]
2060
         *  [
2061
         *    'abc' => [10 => ['aid' => 123, 'code' => 'x-abc']],
2062
         *    'def' => [20 => ['aid' => 123, 'code' => 'x-def'], 30 => ['aid' => 456, 'code' => 'x-def']]
2063
         *  ]
2064
         *  [
2065
         *    '' => [
2066
         *      10 => ['aid' => 123, 'code' => 'x-abc'],
2067
         *      20 => ['aid' => 123, 'code' => 'x-def'],
2068
         *      30 => ['aid' => 456, 'code' => 'x-def']
2069
         *    ]
2070
         *  ]
2071
         *
2072
         * In case the passed key doesn't exist in one or more items, these items
2073
         * are stored in a sub-array using an empty string as key.
2074
         *
2075
         * This does also work for multi-dimensional arrays by passing the keys
2076
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2077
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2078
         * public properties of objects or objects implementing __isset() and __get() methods.
2079
         *
2080
         * @param  \Closure|string|int $key Closure function with (item, idx) parameters returning the key or the key itself to group by
2081
         * @return self<int|string,mixed> New map with elements grouped by the given key
2082
         */
2083
        public function groupBy( \Closure|string|int $key ) : self
2084
        {
2085
                $result = [];
4✔
2086

2087
                if( is_callable( $key ) )
4✔
2088
                {
2089
                        foreach( $this->list() as $idx => $item )
1✔
2090
                        {
2091
                                $keyval = (string) $key( $item, $idx );
1✔
2092
                                $result[$keyval][$idx] = $item;
1✔
2093
                        }
2094
                }
2095
                else
2096
                {
2097
                        $parts = explode( $this->sep, (string) $key );
3✔
2098

2099
                        foreach( $this->list() as $idx => $item )
3✔
2100
                        {
2101
                                $keyval = (string) $this->val( $item, $parts );
3✔
2102
                                $result[$keyval][$idx] = $item;
3✔
2103
                        }
2104
                }
2105

2106
                return new static( $result );
4✔
2107
        }
2108

2109

2110
        /**
2111
         * Determines if a key or several keys exists in the map.
2112
         *
2113
         * If several keys are passed as array, all keys must exist in the map for
2114
         * TRUE to be returned.
2115
         *
2116
         * Examples:
2117
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'a' );
2118
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'b'] );
2119
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->has( 'a/b/c' );
2120
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'c' );
2121
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'c'] );
2122
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'X' );
2123
         *
2124
         * Results:
2125
         * The first three examples will return TRUE while the other ones will return FALSE
2126
         *
2127
         * This does also work for multi-dimensional arrays by passing the keys
2128
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2129
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2130
         * public properties of objects or objects implementing __isset() and __get() methods.
2131
         *
2132
         * @param array<int|string>|int|string $key Key of the requested item or list of keys
2133
         * @return bool TRUE if key or keys are available in map, FALSE if not
2134
         */
2135
        public function has( array|int|string $key ) : bool
2136
        {
2137
                $list = $this->list();
3✔
2138

2139
                foreach( (array) $key as $entry )
3✔
2140
                {
2141
                        if( array_key_exists( $entry, $list ) === false
3✔
2142
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
3✔
2143
                        ) {
2144
                                return false;
3✔
2145
                        }
2146
                }
2147

2148
                return true;
3✔
2149
        }
2150

2151

2152
        /**
2153
         * Executes callbacks depending on the condition.
2154
         *
2155
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2156
         * executed and their returned value is passed back within a Map object. In
2157
         * case no "then" or "else" closure is given, the method will return the same
2158
         * map object.
2159
         *
2160
         * Examples:
2161
         *  Map::from( [] )->if( strpos( 'abc', 'b' ) !== false, function( $map ) {
2162
         *    echo 'found';
2163
         *  } );
2164
         *
2165
         *  Map::from( [] )->if( function( $map ) {
2166
         *    return $map->empty();
2167
         *  }, function( $map ) {
2168
         *    echo 'then';
2169
         *  } );
2170
         *
2171
         *  Map::from( ['a'] )->if( function( $map ) {
2172
         *    return $map->empty();
2173
         *  }, function( $map ) {
2174
         *    echo 'then';
2175
         *  }, function( $map ) {
2176
         *    echo 'else';
2177
         *  } );
2178
         *
2179
         *  Map::from( ['a', 'b'] )->if( true, function( $map ) {
2180
         *    return $map->push( 'c' );
2181
         *  } );
2182
         *
2183
         *  Map::from( ['a', 'b'] )->if( false, null, function( $map ) {
2184
         *    return $map->pop();
2185
         *  } );
2186
         *
2187
         * Results:
2188
         * The first example returns "found" while the second one returns "then" and
2189
         * the third one "else". The forth one will return ['a', 'b', 'c'] while the
2190
         * fifth one will return 'b', which is turned into a map of ['b'] again.
2191
         *
2192
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2193
         * (a short form for anonymous closures) as parameters. The automatically have access
2194
         * to previously defined variables but can not modify them. Also, they can not have
2195
         * a void return type and must/will always return something. Details about
2196
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2197
         *
2198
         * @param \Closure|bool $condition Boolean or function with (map) parameter returning a boolean
2199
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2200
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2201
         * @return self<int|string,mixed> New map
2202
         */
2203
        public function if( \Closure|bool $condition, ?\Closure $then = null, ?\Closure $else = null ) : self
2204
        {
2205
                if( $condition instanceof \Closure ) {
8✔
2206
                        $condition = $condition( $this );
2✔
2207
                }
2208

2209
                if( $condition ) {
8✔
2210
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
5✔
2211
                } elseif( $else ) {
3✔
2212
                        return static::from( $else( $this, $condition ) );
3✔
2213
                }
2214

2215
                return $this;
×
2216
        }
2217

2218

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

2261

2262
        /**
2263
         * Executes callbacks depending if the map is empty or not.
2264
         *
2265
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2266
         * executed and their returned value is passed back within a Map object. In
2267
         * case no "then" or "else" closure is given, the method will return the same
2268
         * map object.
2269
         *
2270
         * Examples:
2271
         *  Map::from( [] )->ifEmpty( function( $map ) {
2272
         *    $map->push( 'a' );
2273
         *  } );
2274
         *
2275
         *  Map::from( ['a'] )->ifEmpty( null, function( $map ) {
2276
         *    return $map->push( 'b' );
2277
         *  } );
2278
         *
2279
         * Results:
2280
         * The first example returns a Map containing ['a'] because the the initial Map
2281
         * is empty. The second one returns  a Map with ['a', 'b'] because the initial
2282
         * Map is not empty and the "else" closure is used.
2283
         *
2284
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2285
         * (a short form for anonymous closures) as parameters. The automatically have access
2286
         * to previously defined variables but can not modify them. Also, they can not have
2287
         * a void return type and must/will always return something. Details about
2288
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2289
         *
2290
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2291
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2292
         * @return self<int|string,mixed> New map
2293
         */
2294
        public function ifEmpty( ?\Closure $then = null, ?\Closure $else = null ) : self
2295
        {
2296
                return $this->if( empty( $this->list() ), $then, $else );
×
2297
        }
2298

2299

2300
        /**
2301
         * Tests if all entries in the map are objects implementing the given interface.
2302
         *
2303
         * Examples:
2304
         *  Map::from( [new Map(), new Map()] )->implements( '\Countable' );
2305
         *  Map::from( [new Map(), new stdClass()] )->implements( '\Countable' );
2306
         *  Map::from( [new Map(), 123] )->implements( '\Countable' );
2307
         *  Map::from( [new Map(), 123] )->implements( '\Countable', true );
2308
         *  Map::from( [new Map(), 123] )->implements( '\Countable', '\RuntimeException' );
2309
         *
2310
         * Results:
2311
         *  The first example returns TRUE while the second and third one return FALSE.
2312
         *  The forth example will throw an UnexpectedValueException while the last one
2313
         *  throws a RuntimeException.
2314
         *
2315
         * @param string $interface Name of the interface that must be implemented
2316
         * @param bool $throw Passing TRUE will throw an exception instead of returning FALSE
2317
         * @return bool TRUE if all entries implement the interface or FALSE if at least one doesn't
2318
         * @throws \UnexpectedValueException If one entry doesn't implement the interface if $throw is TRUE
2319
         */
2320
        public function implements( string $interface, bool $throw = false ) : bool
2321
        {
2322
                foreach( $this->list() as $entry )
3✔
2323
                {
2324
                        if( !( $entry instanceof $interface ) )
3✔
2325
                        {
2326
                                if( $throw ) {
3✔
2327
                                        throw new \UnexpectedValueException( "Does not implement $interface: " . print_r( $entry, true ) );
2✔
2328
                                }
2329

2330
                                return false;
1✔
2331
                        }
2332
                }
2333

2334
                return true;
1✔
2335
        }
2336

2337

2338
        /**
2339
         * Tests if the passed element or elements are part of the map.
2340
         *
2341
         * Examples:
2342
         *  Map::from( ['a', 'b'] )->in( 'a' );
2343
         *  Map::from( ['a', 'b'] )->in( ['a', 'b'] );
2344
         *  Map::from( ['a', 'b'] )->in( 'x' );
2345
         *  Map::from( ['a', 'b'] )->in( ['a', 'x'] );
2346
         *  Map::from( ['1', '2'] )->in( 2, true );
2347
         *
2348
         * Results:
2349
         * The first and second example will return TRUE while the other ones will return FALSE
2350
         *
2351
         * @param array|mixed $element Element or elements to search for in the map
2352
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2353
         * @return bool TRUE if all elements are available in map, FALSE if not
2354
         */
2355
        public function in( mixed $element, bool $strict = false ) : bool
2356
        {
2357
                if( !is_array( $element ) ) {
4✔
2358
                        return in_array( $element, $this->list(), $strict );
4✔
2359
                };
2360

2361
                foreach( $element as $entry )
1✔
2362
                {
2363
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
1✔
2364
                                return false;
1✔
2365
                        }
2366
                }
2367

2368
                return true;
1✔
2369
        }
2370

2371

2372
        /**
2373
         * Tests if the passed element or elements are part of the map.
2374
         *
2375
         * This method is an alias for in(). For performance reasons, in() should be
2376
         * preferred because it uses one method call less than includes().
2377
         *
2378
         * @param mixed|array $element Element or elements to search for in the map
2379
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2380
         * @return bool TRUE if all elements are available in map, FALSE if not
2381
         * @see in() - Underlying method with same parameters and return value but better performance
2382
         */
2383
        public function includes( mixed $element, bool $strict = false ) : bool
2384
        {
2385
                return $this->in( $element, $strict );
1✔
2386
        }
2387

2388

2389
        /**
2390
         * Returns the numerical index of the given key.
2391
         *
2392
         * Examples:
2393
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( '8' );
2394
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( function( $key ) {
2395
         *      return $key == '8';
2396
         *  } );
2397
         *
2398
         * Results:
2399
         * Both examples will return "1" because the value "b" is at the second position
2400
         * and the returned index is zero based so the first item has the index "0".
2401
         *
2402
         * @param \Closure|string|int $value Key to search for or function with (key) parameters return TRUE if key is found
2403
         * @return int|null Position of the found value (zero based) or NULL if not found
2404
         */
2405
        public function index( \Closure|string|int $value ) : ?int
2406
        {
2407
                if( $value instanceof \Closure )
4✔
2408
                {
2409
                        $pos = 0;
2✔
2410

2411
                        foreach( $this->list() as $key => $item )
2✔
2412
                        {
2413
                                if( $value( $key ) ) {
1✔
2414
                                        return $pos;
1✔
2415
                                }
2416

2417
                                ++$pos;
1✔
2418
                        }
2419

2420
                        return null;
1✔
2421
                }
2422

2423
                $pos = array_search( $value, array_keys( $this->list() ) );
2✔
2424
                return $pos !== false ? $pos : null;
2✔
2425
        }
2426

2427

2428
        /**
2429
         * Inserts the value or values after the given element.
2430
         *
2431
         * Examples:
2432
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAfter( 'foo', 'baz' );
2433
         *  Map::from( ['foo', 'bar'] )->insertAfter( 'foo', ['baz', 'boo'] );
2434
         *  Map::from( ['foo', 'bar'] )->insertAfter( null, 'baz' );
2435
         *
2436
         * Results:
2437
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2438
         *  ['foo', 'baz', 'boo', 'bar']
2439
         *  ['foo', 'bar', 'baz']
2440
         *
2441
         * Numerical array indexes are not preserved.
2442
         *
2443
         * @param mixed $element Element after the value is inserted
2444
         * @param mixed $value Element or list of elements to insert
2445
         * @return self<int|string,mixed> Updated map for fluid interface
2446
         */
2447
        public function insertAfter( mixed $element, mixed $value ) : self
2448
        {
2449
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
3✔
2450
                array_splice( $this->list(), $position + 1, 0, $this->array( $value ) );
3✔
2451

2452
                return $this;
3✔
2453
        }
2454

2455

2456
        /**
2457
         * Inserts the item at the given position in the map.
2458
         *
2459
         * Examples:
2460
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 0, 'baz' );
2461
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 1, 'baz', 'c' );
2462
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 4, 'baz' );
2463
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( -1, 'baz', 'c' );
2464
         *
2465
         * Results:
2466
         *  [0 => 'baz', 'a' => 'foo', 'b' => 'bar']
2467
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2468
         *  ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']
2469
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2470
         *
2471
         * @param int $pos Position the value should be inserted at
2472
         * @param mixed $value Value to be inserted
2473
         * @param string|int|null $key Value key or NULL to assign an integer key automatically
2474
         * @return self<int|string,mixed> Updated map for fluid interface
2475
         */
2476
        public function insertAt( int $pos, mixed $value, string|int|null $key = null ) : self
2477
        {
2478
                if( $key !== null )
5✔
2479
                {
2480
                        $list = $this->list();
2✔
2481

2482
                        $this->list = array_merge(
2✔
2483
                                array_slice( $list, 0, $pos, true ),
2✔
2484
                                [$key => $value],
2✔
2485
                                array_slice( $list, $pos, null, true )
2✔
2486
                        );
2✔
2487
                }
2488
                else
2489
                {
2490
                        array_splice( $this->list(), $pos, 0, [$value] );
3✔
2491
                }
2492

2493
                return $this;
5✔
2494
        }
2495

2496

2497
        /**
2498
         * Inserts the value or values before the given element.
2499
         *
2500
         * Examples:
2501
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertBefore( 'bar', 'baz' );
2502
         *  Map::from( ['foo', 'bar'] )->insertBefore( 'bar', ['baz', 'boo'] );
2503
         *  Map::from( ['foo', 'bar'] )->insertBefore( null, 'baz' );
2504
         *
2505
         * Results:
2506
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2507
         *  ['foo', 'baz', 'boo', 'bar']
2508
         *  ['foo', 'bar', 'baz']
2509
         *
2510
         * Numerical array indexes are not preserved.
2511
         *
2512
         * @param mixed $element Element before the value is inserted
2513
         * @param mixed $value Element or list of elements to insert
2514
         * @return self<int|string,mixed> Updated map for fluid interface
2515
         */
2516
        public function insertBefore( mixed $element, mixed $value ) : self
2517
        {
2518
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
3✔
2519
                array_splice( $this->list(), $position, 0, $this->array( $value ) );
3✔
2520

2521
                return $this;
3✔
2522
        }
2523

2524

2525
        /**
2526
         * Tests if the passed value or values are part of the strings in the map.
2527
         *
2528
         * Examples:
2529
         *  Map::from( ['abc'] )->inString( 'c' );
2530
         *  Map::from( ['abc'] )->inString( 'bc' );
2531
         *  Map::from( [12345] )->inString( '23' );
2532
         *  Map::from( [123.4] )->inString( 23.4 );
2533
         *  Map::from( [12345] )->inString( false );
2534
         *  Map::from( [12345] )->inString( true );
2535
         *  Map::from( [false] )->inString( false );
2536
         *  Map::from( ['abc'] )->inString( '' );
2537
         *  Map::from( [''] )->inString( false );
2538
         *  Map::from( ['abc'] )->inString( 'BC', false );
2539
         *  Map::from( ['abc', 'def'] )->inString( ['de', 'xy'] );
2540
         *  Map::from( ['abc', 'def'] )->inString( ['E', 'x'] );
2541
         *  Map::from( ['abc', 'def'] )->inString( 'E' );
2542
         *  Map::from( [23456] )->inString( true );
2543
         *  Map::from( [false] )->inString( 0 );
2544
         *
2545
         * Results:
2546
         * The first eleven examples will return TRUE while the last four will return FALSE
2547
         *
2548
         * All scalar values (bool, float, int and string) are casted to string values before
2549
         * comparing to the given value. Non-scalar values in the map are ignored.
2550
         *
2551
         * @param array<string>|string $value Value or values to compare the map elements, will be casted to string type
2552
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
2553
         * @return bool TRUE If at least one element matches, FALSE if value is not in any string of the map
2554
         * @deprecated Use multi-byte aware strContains() instead
2555
         */
2556
        public function inString( array|string $value, bool $case = true ) : bool
2557
        {
2558
                $fcn = $case ? 'strpos' : 'stripos';
1✔
2559

2560
                foreach( (array) $value as $val )
1✔
2561
                {
2562
                        if( (string) $val === '' ) {
1✔
2563
                                return true;
1✔
2564
                        }
2565

2566
                        foreach( $this->list() as $item )
1✔
2567
                        {
2568
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
1✔
2569
                                        return true;
1✔
2570
                                }
2571
                        }
2572
                }
2573

2574
                return false;
1✔
2575
        }
2576

2577

2578
        /**
2579
         * Returns an element by key and casts it to integer if possible.
2580
         *
2581
         * Examples:
2582
         *  Map::from( ['a' => true] )->int( 'a' );
2583
         *  Map::from( ['a' => '1'] )->int( 'a' );
2584
         *  Map::from( ['a' => 1.1] )->int( 'a' );
2585
         *  Map::from( ['a' => '10'] )->int( 'a' );
2586
         *  Map::from( ['a' => ['b' => ['c' => 1]]] )->int( 'a/b/c' );
2587
         *  Map::from( [] )->int( 'c', function( $val ) { return rand( 1, 1 ); } );
2588
         *  Map::from( [] )->int( 'a', 1 );
2589
         *
2590
         *  Map::from( [] )->int( 'b' );
2591
         *  Map::from( ['b' => ''] )->int( 'b' );
2592
         *  Map::from( ['b' => 'abc'] )->int( 'b' );
2593
         *  Map::from( ['b' => null] )->int( 'b' );
2594
         *  Map::from( ['b' => [1]] )->int( 'b' );
2595
         *  Map::from( ['b' => #resource] )->int( 'b' );
2596
         *  Map::from( ['b' => new \stdClass] )->int( 'b' );
2597
         *
2598
         *  Map::from( [] )->int( 'c', new \Exception( 'error' ) );
2599
         *
2600
         * Results:
2601
         * The first seven examples will return 1 while the 8th to 14th example
2602
         * returns 0. The last example will throw an exception.
2603
         *
2604
         * This does also work for multi-dimensional arrays by passing the keys
2605
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2606
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2607
         * public properties of objects or objects implementing __isset() and __get() methods.
2608
         *
2609
         * @param int|string $key Key or path to the requested item
2610
         * @param \Closure|\Throwable|int $default Default value if key isn't found
2611
         * @return int Value from map or default value
2612
         */
2613
        public function int( int|string $key, \Closure|\Throwable|int $default = 0 ) : int
2614
        {
2615
                if( is_scalar( $val = $this->get( $key, $default ) ) ) {
3✔
2616
                        return (int) $val;
2✔
2617
                }
2618

2619
                if( $default instanceof \Closure ) {
1✔
2620
                        $default = $default( $val );
×
2621
                }
2622

2623
                if( $default instanceof \Throwable ) {
1✔
2624
                        throw $default;
×
2625
                }
2626

2627
                return (int) $default;
1✔
2628
        }
2629

2630

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

2665
                if( $callback ) {
2✔
2666
                        return new static( array_uintersect( $list, $elements, $callback ) );
1✔
2667
                }
2668

2669
                return new static( array_intersect( $list, $elements ) );
1✔
2670
        }
2671

2672

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

2707
                if( $callback ) {
5✔
2708
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
1✔
2709
                }
2710

2711
                return new static( array_intersect_assoc( $this->list(), $elements ) );
4✔
2712
        }
2713

2714

2715
        /**
2716
         * Returns all values in a new map that are available in both, the map and the given elements by comparing the keys only.
2717
         *
2718
         * Examples:
2719
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectKeys( new Map( ['foo', 'b' => 'baz'] ) );
2720
         *
2721
         * Results:
2722
         *  ['b' => 'bar']
2723
         *
2724
         * If a callback is passed, the given function will be used to compare the keys.
2725
         * The function must accept two parameters (key A and B) and must return
2726
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
2727
         * greater than key B. Both, a method name and an anonymous function can be passed:
2728
         *
2729
         *  Map::from( [0 => 'a'] )->intersectKeys( [0 => 'A'], 'strcasecmp' );
2730
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['B' => 'X'], 'strcasecmp' );
2731
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['c' => 'a'], function( $keyA, $keyB ) {
2732
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
2733
         *  } );
2734
         *
2735
         * The first example will return a map with [0 => 'a'] and the second one will
2736
         * return a map with ['b' => 'a'] because both contain the same keys when compared
2737
         * case insensitive. The third example will return an empty map because the keys
2738
         * doesn't match ("b" vs. "c").
2739
         *
2740
         * The keys are preserved using this method.
2741
         *
2742
         * @param iterable<int|string,mixed> $elements List of elements
2743
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
2744
         * @return self<int|string,mixed> New map
2745
         */
2746
        public function intersectKeys( iterable $elements, ?callable $callback = null ) : self
2747
        {
2748
                $elements = $this->array( $elements );
3✔
2749

2750
                if( $callback ) {
3✔
2751
                        return new static( array_intersect_ukey( $this->list(), $elements, $callback ) );
1✔
2752
                }
2753

2754
                return new static( array_intersect_key( $this->list(), $elements ) );
2✔
2755
        }
2756

2757

2758
        /**
2759
         * Tests if the map consists of the same keys and values
2760
         *
2761
         * Examples:
2762
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'] );
2763
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'], true );
2764
         *  Map::from( [1, 2] )->is( ['1', '2'] );
2765
         *
2766
         * Results:
2767
         *  The first example returns TRUE while the second and third one returns FALSE
2768
         *
2769
         * @param iterable<int|string,mixed> $list List of key/value pairs to compare with
2770
         * @param bool $strict TRUE for comparing order of elements too, FALSE for key/values only
2771
         * @return bool TRUE if given list is equal, FALSE if not
2772
         */
2773
        public function is( iterable $list, bool $strict = false ) : bool
2774
        {
2775
                $list = $this->array( $list );
3✔
2776

2777
                if( $strict ) {
3✔
2778
                        return $this->list() === $list;
2✔
2779
                }
2780

2781
                return $this->list() == $list;
1✔
2782
        }
2783

2784

2785
        /**
2786
         * Determines if the map is empty or not.
2787
         *
2788
         * Examples:
2789
         *  Map::from( [] )->isEmpty();
2790
         *  Map::from( ['a'] )->isEmpty();
2791
         *
2792
         * Results:
2793
         *  The first example returns TRUE while the second returns FALSE
2794
         *
2795
         * The method is equivalent to empty().
2796
         *
2797
         * @return bool TRUE if map is empty, FALSE if not
2798
         */
2799
        public function isEmpty() : bool
2800
        {
2801
                return empty( $this->list() );
4✔
2802
        }
2803

2804

2805
        /**
2806
         * Checks if the map contains a list of subsequentially numbered keys.
2807
         *
2808
         * Examples:
2809
         * Map::from( [] )->isList();
2810
         * Map::from( [1, 3, 2] )->isList();
2811
         * Map::from( [0 => 1, 1 => 2, 2 => 3] )->isList();
2812
         * Map::from( [1 => 1, 2 => 2, 3 => 3] )->isList();
2813
         * Map::from( [0 => 1, 2 => 2, 3 => 3] )->isList();
2814
         * Map::from( ['a' => 1, 1 => 2, 'c' => 3] )->isList();
2815
         *
2816
         * Results:
2817
         * The first three examples return TRUE while the last three return FALSE
2818
         *
2819
         * @return bool TRUE if the map is a list, FALSE if not
2820
         */
2821
        public function isList() : bool
2822
        {
2823
                $i = -1;
1✔
2824

2825
                foreach( $this->list() as $k => $v )
1✔
2826
                {
2827
                        if( $k !== ++$i ) {
1✔
2828
                                return false;
1✔
2829
                        }
2830
                }
2831

2832
                return true;
1✔
2833
        }
2834

2835

2836
        /**
2837
         * Determines if all entries are numeric values.
2838
         *
2839
         * Examples:
2840
         *  Map::from( [] )->isNumeric();
2841
         *  Map::from( [1] )->isNumeric();
2842
         *  Map::from( [1.1] )->isNumeric();
2843
         *  Map::from( [010] )->isNumeric();
2844
         *  Map::from( [0x10] )->isNumeric();
2845
         *  Map::from( [0b10] )->isNumeric();
2846
         *  Map::from( ['010'] )->isNumeric();
2847
         *  Map::from( ['10'] )->isNumeric();
2848
         *  Map::from( ['10.1'] )->isNumeric();
2849
         *  Map::from( [' 10 '] )->isNumeric();
2850
         *  Map::from( ['10e2'] )->isNumeric();
2851
         *  Map::from( ['0b10'] )->isNumeric();
2852
         *  Map::from( ['0x10'] )->isNumeric();
2853
         *  Map::from( ['null'] )->isNumeric();
2854
         *  Map::from( [null] )->isNumeric();
2855
         *  Map::from( [true] )->isNumeric();
2856
         *  Map::from( [[]] )->isNumeric();
2857
         *  Map::from( [''] )->isNumeric();
2858
         *
2859
         * Results:
2860
         *  The first eleven examples return TRUE while the last seven return FALSE
2861
         *
2862
         * @return bool TRUE if all map entries are numeric values, FALSE if not
2863
         */
2864
        public function isNumeric() : bool
2865
        {
2866
                foreach( $this->list() as $val )
1✔
2867
                {
2868
                        if( !is_numeric( $val ) ) {
1✔
2869
                                return false;
1✔
2870
                        }
2871
                }
2872

2873
                return true;
1✔
2874
        }
2875

2876

2877
        /**
2878
         * Determines if all entries are objects.
2879
         *
2880
         * Examples:
2881
         *  Map::from( [] )->isObject();
2882
         *  Map::from( [new stdClass] )->isObject();
2883
         *  Map::from( [1] )->isObject();
2884
         *
2885
         * Results:
2886
         *  The first two examples return TRUE while the last one return FALSE
2887
         *
2888
         * @return bool TRUE if all map entries are objects, FALSE if not
2889
         */
2890
        public function isObject() : bool
2891
        {
2892
                foreach( $this->list() as $val )
1✔
2893
                {
2894
                        if( !is_object( $val ) ) {
1✔
2895
                                return false;
1✔
2896
                        }
2897
                }
2898

2899
                return true;
1✔
2900
        }
2901

2902

2903
        /**
2904
         * Determines if all entries are scalar values.
2905
         *
2906
         * Examples:
2907
         *  Map::from( [] )->isScalar();
2908
         *  Map::from( [1] )->isScalar();
2909
         *  Map::from( [1.1] )->isScalar();
2910
         *  Map::from( ['abc'] )->isScalar();
2911
         *  Map::from( [true, false] )->isScalar();
2912
         *  Map::from( [new stdClass] )->isScalar();
2913
         *  Map::from( [#resource] )->isScalar();
2914
         *  Map::from( [null] )->isScalar();
2915
         *  Map::from( [[1]] )->isScalar();
2916
         *
2917
         * Results:
2918
         *  The first five examples return TRUE while the others return FALSE
2919
         *
2920
         * @return bool TRUE if all map entries are scalar values, FALSE if not
2921
         */
2922
        public function isScalar() : bool
2923
        {
2924
                foreach( $this->list() as $val )
1✔
2925
                {
2926
                        if( !is_scalar( $val ) ) {
1✔
2927
                                return false;
1✔
2928
                        }
2929
                }
2930

2931
                return true;
1✔
2932
        }
2933

2934

2935
        /**
2936
         * Tests for the matching item, but is true only if exactly one item is matching.
2937
         *
2938
         * Examples:
2939
         *  Map::from( ['a', 'b'] )->isSole( 'a' );
2940
         *  Map::from( ['a', 'b', 'a'] )->isSole( fn( $v, $k ) => $v === 'a' && $k < 2 );
2941
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->isSole( fn( $v, $k ) => $v['name'] === 'user' );
2942
         *  Map::from( ['b', 'c'] )->isSole( 'a' );
2943
         *  Map::from( ['a', 'b', 'a'] )->isSole( 'a' );
2944
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->isSole( 'test', 'name' );
2945
         *
2946
         * Results:
2947
         * The first three examples will return TRUE while all others will return FALSE.
2948
         *
2949
         * @param \Closure|mixed $value Closure with (item, key) parameter or element to test against
2950
         * @param string|int|null $key Key to compare the value for if $values is not a closure
2951
         * @return bool TRUE if exactly one item matches, FALSE if no or more than one item matches
2952
         */
2953
        public function isSole( mixed $value = null, string|int|null $key = null ) : bool
2954
        {
2955
                return $this->restrict( $value, $key )->count() === 1;
3✔
2956
        }
2957

2958

2959
        /**
2960
         * Determines if all entries are string values.
2961
         *
2962
         * Examples:
2963
         *  Map::from( ['abc'] )->isString();
2964
         *  Map::from( [] )->isString();
2965
         *  Map::from( [1] )->isString();
2966
         *  Map::from( [1.1] )->isString();
2967
         *  Map::from( [true, false] )->isString();
2968
         *  Map::from( [new stdClass] )->isString();
2969
         *  Map::from( [#resource] )->isString();
2970
         *  Map::from( [null] )->isString();
2971
         *  Map::from( [[1]] )->isString();
2972
         *
2973
         * Results:
2974
         *  The first two examples return TRUE while the others return FALSE
2975
         *
2976
         * @return bool TRUE if all map entries are string values, FALSE if not
2977
         */
2978
        public function isString() : bool
2979
        {
2980
                foreach( $this->list() as $val )
1✔
2981
                {
2982
                        if( !is_string( $val ) ) {
1✔
2983
                                return false;
1✔
2984
                        }
2985
                }
2986

2987
                return true;
1✔
2988
        }
2989

2990

2991
        /**
2992
         * Concatenates the string representation of all elements.
2993
         *
2994
         * Objects that implement __toString() does also work, otherwise (and in case
2995
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2996
         * empty strings.
2997
         *
2998
         * Examples:
2999
         *  Map::from( ['a', 'b', false] )->join();
3000
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
3001
         *
3002
         * Results:
3003
         * The first example will return "ab" while the second one will return "a-b--"
3004
         *
3005
         * @param string $glue Character or string added between elements
3006
         * @return string String of concatenated map elements
3007
         */
3008
        public function join( string $glue = '' ) : string
3009
        {
3010
                return implode( $glue, $this->list() );
1✔
3011
        }
3012

3013

3014
        /**
3015
         * Specifies the data which should be serialized to JSON by json_encode().
3016
         *
3017
         * Examples:
3018
         *   json_encode( Map::from( ['a', 'b'] ) );
3019
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
3020
         *
3021
         * Results:
3022
         *   ["a", "b"]
3023
         *   {"a":0,"b":1}
3024
         *
3025
         * @return array<int|string,mixed> Data to serialize to JSON
3026
         */
3027
        public function jsonSerialize() : mixed
3028
        {
3029
                return $this->list = $this->array( $this->list );
1✔
3030
        }
3031

3032

3033
        /**
3034
         * Returns the keys of the all elements in a new map object.
3035
         *
3036
         * Examples:
3037
         *  Map::from( ['a', 'b'] );
3038
         *  Map::from( ['a' => 0, 'b' => 1] );
3039
         *
3040
         * Results:
3041
         * The first example returns a map containing [0, 1] while the second one will
3042
         * return a map with ['a', 'b'].
3043
         *
3044
         * @return self<int|string,mixed> New map
3045
         */
3046
        public function keys() : self
3047
        {
3048
                return new static( array_keys( $this->list() ) );
1✔
3049
        }
3050

3051

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

3082

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

3112

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

3143

3144
        /**
3145
         * Sorts a copy of the elements by their keys.
3146
         *
3147
         * Examples:
3148
         *  Map::from( ['b' => 0, 'a' => 1] )->ksorted();
3149
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksorted();
3150
         *
3151
         * Results:
3152
         *  ['a' => 1, 'b' => 0]
3153
         *  [0 => 'b', 1 => 'a']
3154
         *
3155
         * The parameter modifies how the keys are compared. Possible values are:
3156
         * - SORT_REGULAR : compare elements normally (don't change types)
3157
         * - SORT_NUMERIC : compare elements numerically
3158
         * - SORT_STRING : compare elements as strings
3159
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3160
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3161
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3162
         *
3163
         * The keys are preserved using this method and no new map is created.
3164
         *
3165
         * @param int $options Sort options for ksort()
3166
         * @return self<int|string,mixed> Updated map for fluid interface
3167
         */
3168
        public function ksorted( int $options = SORT_REGULAR ) : self
3169
        {
3170
                return ( clone $this )->ksort();
1✔
3171
        }
3172

3173

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

3200
                if( $default instanceof \Closure ) {
4✔
3201
                        return $default();
1✔
3202
                }
3203

3204
                if( $default instanceof \Throwable ) {
3✔
3205
                        throw $default;
1✔
3206
                }
3207

3208
                return $default;
2✔
3209
        }
3210

3211

3212
        /**
3213
         * Returns the last key from the map.
3214
         *
3215
         * Examples:
3216
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
3217
         *  Map::from( [] )->lastKey( 'x' );
3218
         *  Map::from( [] )->lastKey( new \Exception( 'error' ) );
3219
         *  Map::from( [] )->lastKey( function() { return rand(); } );
3220
         *
3221
         * Results:
3222
         * The first example will return 'a' and the second one 'x', the third one will throw
3223
         * an exception and the last one will return a random value.
3224
         *
3225
         * Using this method doesn't affect the internal array pointer.
3226
         *
3227
         * @param mixed $default Default value, closure or exception if the map contains no elements
3228
         * @return mixed Last key of map, (generated) default value or an exception
3229
         */
3230
        public function lastKey( mixed $default = null ) : mixed
3231
        {
3232
                $list = $this->list();
5✔
3233

3234
                // PHP 7.x compatibility
3235
                if( function_exists( 'array_key_last' ) ) {
5✔
3236
                        $key = array_key_last( $list );
5✔
3237
                } else {
3238
                        $key = key( array_slice( $list, -1, 1, true ) );
×
3239
                }
3240

3241
                if( $key !== null ) {
5✔
3242
                        return $key;
1✔
3243
                }
3244

3245
                if( $default instanceof \Closure ) {
4✔
3246
                        return $default();
1✔
3247
                }
3248

3249
                if( $default instanceof \Throwable ) {
3✔
3250
                        throw $default;
1✔
3251
                }
3252

3253
                return $default;
2✔
3254
        }
3255

3256

3257
        /**
3258
         * Removes the passed characters from the left of all strings.
3259
         *
3260
         * Examples:
3261
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
3262
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
3263
         *
3264
         * Results:
3265
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
3266
         *
3267
         * @param string $chars List of characters to trim
3268
         * @return self<int|string,mixed> Updated map for fluid interface
3269
         */
3270
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
3271
        {
3272
                foreach( $this->list() as &$entry )
1✔
3273
                {
3274
                        if( is_string( $entry ) ) {
1✔
3275
                                $entry = ltrim( $entry, $chars );
1✔
3276
                        }
3277
                }
3278

3279
                return $this;
1✔
3280
        }
3281

3282

3283
        /**
3284
         * Maps new values to the existing keys using the passed function and returns a new map for the result.
3285
         *
3286
         * Examples:
3287
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
3288
         *      return $value * 2;
3289
         *  } );
3290
         *
3291
         * Results:
3292
         *  ['a' => 4, 'b' => 8]
3293
         *
3294
         * The keys are preserved using this method.
3295
         *
3296
         * @param callable $callback Function with (value, key) parameters and returns computed result
3297
         * @return self<int|string,mixed> New map with the original keys and the computed values
3298
         * @see rekey() - Changes the keys according to the passed function
3299
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3300
         */
3301
        public function map( callable $callback ) : self
3302
        {
3303
                $list = $this->list();
1✔
3304
                $keys = array_keys( $list );
1✔
3305
                $map = array_map( $callback, array_values( $list ), $keys );
1✔
3306

3307
                return new static( array_combine( $keys, $map ) );
1✔
3308
        }
3309

3310

3311
        /**
3312
         * Returns the maximum value of all elements.
3313
         *
3314
         * Examples:
3315
         *  Map::from( [1, 3, 2, 5, 4] )->max()
3316
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
3317
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
3318
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
3319
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( fn( $val, $key ) => $val['i']['p'] ?? null )
3320
         *  Map::from( [50, 10, 30] )->max( fn( $val, $key ) => $key > 0 ? $val : null )
3321
         *
3322
         * Results:
3323
         * The first line will return "5", the second one "foo" and the third to fitfh
3324
         * one return 50 while the last one will return 30.
3325
         *
3326
         * NULL values are removed before the comparison. If there are no values or all
3327
         * values are NULL, NULL is returned.
3328
         *
3329
         * This does also work for multi-dimensional arrays by passing the keys
3330
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3331
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3332
         * public properties of objects or objects implementing __isset() and __get() methods.
3333
         *
3334
         * Be careful comparing elements of different types because this can have
3335
         * unpredictable results due to the PHP comparison rules:
3336
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3337
         *
3338
         * @param \Closure|string|null $col Closure, key or path to the value of the nested array or object
3339
         * @return mixed Maximum value or NULL if there are no elements in the map
3340
         */
3341
        public function max( \Closure|string|null $col = null ) : mixed
3342
        {
3343
                $list = $this->list();
4✔
3344
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list );
4✔
3345

3346
                return !empty( $vals ) ? max( $vals ) : null;
4✔
3347
        }
3348

3349

3350
        /**
3351
         * Merges the map with the given elements without returning a new map.
3352
         *
3353
         * Elements with the same non-numeric keys will be overwritten, elements
3354
         * with the same numeric keys will be added.
3355
         *
3356
         * Examples:
3357
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
3358
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
3359
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
3360
         *
3361
         * Results:
3362
         *  ['a', 'b', 'b', 'c']
3363
         *  ['a' => 1, 'b' => 4, 'c' => 6]
3364
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
3365
         *
3366
         * The method is similar to replace() but doesn't replace elements with
3367
         * the same numeric keys. If you want to be sure that all passed elements
3368
         * are added without replacing existing ones, use concat() instead.
3369
         *
3370
         * The keys are preserved using this method.
3371
         *
3372
         * @param iterable<int|string,mixed> $elements List of elements
3373
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
3374
         * @return self<int|string,mixed> Updated map for fluid interface
3375
         */
3376
        public function merge( iterable $elements, bool $recursive = false ) : self
3377
        {
3378
                if( $recursive ) {
3✔
3379
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
1✔
3380
                } else {
3381
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
2✔
3382
                }
3383

3384
                return $this;
3✔
3385
        }
3386

3387

3388
        /**
3389
         * Returns the minimum value of all elements.
3390
         *
3391
         * Examples:
3392
         *  Map::from( [2, 3, 1, 5, 4] )->min()
3393
         *  Map::from( ['baz', 'foo', 'bar'] )->min()
3394
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->min( 'p' )
3395
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( 'i/p' )
3396
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( fn( $val, $key ) => $val['i']['p'] ?? null )
3397
         *  Map::from( [10, 50, 30] )->min( fn( $val, $key ) => $key > 0 ? $val : null )
3398
         *
3399
         * Results:
3400
         * The first line will return "1", the second one "bar", the third one
3401
         * 10, and the forth to the last one 30.
3402
         *
3403
         * NULL values are removed before the comparison. If there are no values or all
3404
         * values are NULL, NULL is returned.
3405
         *
3406
         * This does also work for multi-dimensional arrays by passing the keys
3407
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3408
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3409
         * public properties of objects or objects implementing __isset() and __get() methods.
3410
         *
3411
         * Be careful comparing elements of different types because this can have
3412
         * unpredictable results due to the PHP comparison rules:
3413
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3414
         *
3415
         * @param \Closure|string|null $col Closure, key or path to the value of the nested array or object
3416
         * @return mixed Minimum value or NULL if there are no elements in the map
3417
         */
3418
        public function min( \Closure|string|null $col = null ) : mixed
3419
        {
3420
                $list = $this->list();
4✔
3421
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list );
4✔
3422

3423
                return !empty( $vals ) ? min( $vals ) : null;
4✔
3424
        }
3425

3426

3427
        /**
3428
         * Tests if none of the elements are part of the map.
3429
         *
3430
         * Examples:
3431
         *  Map::from( ['a', 'b'] )->none( 'x' );
3432
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
3433
         *  Map::from( ['1', '2'] )->none( 2, true );
3434
         *  Map::from( ['a', 'b'] )->none( 'a' );
3435
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
3436
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
3437
         *
3438
         * Results:
3439
         * The first three examples will return TRUE while the other ones will return FALSE
3440
         *
3441
         * @param mixed|array $element Element or elements to search for in the map
3442
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3443
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
3444
         */
3445
        public function none( mixed $element, bool $strict = false ) : bool
3446
        {
3447
                $list = $this->list();
1✔
3448

3449
                if( !is_array( $element ) ) {
1✔
3450
                        return !in_array( $element, $list, $strict );
1✔
3451
                };
3452

3453
                foreach( $element as $entry )
1✔
3454
                {
3455
                        if( in_array( $entry, $list, $strict ) === true ) {
1✔
3456
                                return false;
1✔
3457
                        }
3458
                }
3459

3460
                return true;
1✔
3461
        }
3462

3463

3464
        /**
3465
         * Returns every nth element from the map.
3466
         *
3467
         * Examples:
3468
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
3469
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
3470
         *
3471
         * Results:
3472
         *  ['a', 'c', 'e']
3473
         *  ['b', 'd', 'f']
3474
         *
3475
         * @param int $step Step width
3476
         * @param int $offset Number of element to start from (0-based)
3477
         * @return self<int|string,mixed> New map
3478
         */
3479
        public function nth( int $step, int $offset = 0 ) : self
3480
        {
3481
                if( $step < 1 ) {
3✔
3482
                        throw new \InvalidArgumentException( 'Step width must be greater than zero' );
1✔
3483
                }
3484

3485
                if( $step === 1 ) {
2✔
3486
                        return clone $this;
1✔
3487
                }
3488

3489
                $result = [];
1✔
3490
                $list = $this->list();
1✔
3491

3492
                while( !empty( $pair = array_slice( $list, $offset, 1, true ) ) )
1✔
3493
                {
3494
                        $result += $pair;
1✔
3495
                        $offset += $step;
1✔
3496
                }
3497

3498
                return new static( $result );
1✔
3499
        }
3500

3501

3502
        /**
3503
         * Determines if an element exists at an offset.
3504
         *
3505
         * Examples:
3506
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3507
         *  isset( $map['b'] );
3508
         *  isset( $map['c'] );
3509
         *  isset( $map['d'] );
3510
         *
3511
         * Results:
3512
         *  The first isset() will return TRUE while the second and third one will return FALSE
3513
         *
3514
         * @param int|string $key Key to check for
3515
         * @return bool TRUE if key exists, FALSE if not
3516
         */
3517
        public function offsetExists( mixed $key ) : bool
3518
        {
3519
                return isset( $this->list()[$key] );
7✔
3520
        }
3521

3522

3523
        /**
3524
         * Returns an element at a given offset.
3525
         *
3526
         * Examples:
3527
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3528
         *  $map['b'];
3529
         *
3530
         * Results:
3531
         *  $map['b'] will return 3
3532
         *
3533
         * @param int|string $key Key to return the element for
3534
         * @return mixed Value associated to the given key
3535
         */
3536
        public function offsetGet( mixed $key ) : mixed
3537
        {
3538
                return $this->list()[$key] ?? null;
5✔
3539
        }
3540

3541

3542
        /**
3543
         * Sets the element at a given offset.
3544
         *
3545
         * Examples:
3546
         *  $map = Map::from( ['a' => 1] );
3547
         *  $map['b'] = 2;
3548
         *  $map[0] = 4;
3549
         *
3550
         * Results:
3551
         *  ['a' => 1, 'b' => 2, 0 => 4]
3552
         *
3553
         * @param int|string|null $key Key to set the element for or NULL to append value
3554
         * @param mixed $value New value set for the key
3555
         */
3556
        public function offsetSet( mixed $key, mixed $value ) : void
3557
        {
3558
                if( $key !== null ) {
3✔
3559
                        $this->list()[$key] = $value;
2✔
3560
                } else {
3561
                        $this->list()[] = $value;
2✔
3562
                }
3563
        }
3564

3565

3566
        /**
3567
         * Unsets the element at a given offset.
3568
         *
3569
         * Examples:
3570
         *  $map = Map::from( ['a' => 1] );
3571
         *  unset( $map['a'] );
3572
         *
3573
         * Results:
3574
         *  The map will be empty
3575
         *
3576
         * @param int|string $key Key for unsetting the item
3577
         */
3578
        public function offsetUnset( mixed $key ) : void
3579
        {
3580
                unset( $this->list()[$key] );
2✔
3581
        }
3582

3583

3584
        /**
3585
         * Returns a new map with only those elements specified by the given keys.
3586
         *
3587
         * Examples:
3588
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3589
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3590
         *
3591
         * Results:
3592
         *  ['a' => 1]
3593
         *  [0 => 'b', 1 => 'c']
3594
         *
3595
         * The keys are preserved using this method.
3596
         *
3597
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3598
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3599
         */
3600
        public function only( iterable|string|int $keys ) : self
3601
        {
3602
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
1✔
3603
        }
3604

3605

3606
        /**
3607
         * Returns a new map with elements ordered by the passed keys.
3608
         *
3609
         * If there are less keys passed than available in the map, the remaining
3610
         * elements are removed. Otherwise, if keys are passed that are not in the
3611
         * map, they will be also available in the returned map but their value is
3612
         * NULL.
3613
         *
3614
         * Examples:
3615
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3616
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3617
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3618
         *
3619
         * Results:
3620
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3621
         *  [0 => 'b', 1 => 'c', 2 => null]
3622
         *  [0 => 'b', 1 => 'c']
3623
         *
3624
         * The keys are preserved using this method.
3625
         *
3626
         * @param iterable<mixed> $keys Keys of the elements in the required order
3627
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3628
         */
3629
        public function order( iterable $keys ) : self
3630
        {
3631
                $result = [];
1✔
3632
                $list = $this->list();
1✔
3633

3634
                foreach( $keys as $key ) {
1✔
3635
                        $result[$key] = $list[$key] ?? null;
1✔
3636
                }
3637

3638
                return new static( $result );
1✔
3639
        }
3640

3641

3642
        /**
3643
         * Fill up to the specified length with the given value
3644
         *
3645
         * In case the given number is smaller than the number of element that are
3646
         * already in the list, the map is unchanged. If the size is positive, the
3647
         * new elements are padded on the right, if it's negative then the elements
3648
         * are padded on the left.
3649
         *
3650
         * Examples:
3651
         *  Map::from( [1, 2, 3] )->pad( 5 );
3652
         *  Map::from( [1, 2, 3] )->pad( -5 );
3653
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3654
         *  Map::from( [1, 2, 3] )->pad( 2 );
3655
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3656
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3657
         *
3658
         * Results:
3659
         *  [1, 2, 3, null, null]
3660
         *  [null, null, 1, 2, 3]
3661
         *  [1, 2, 3, '0', '0']
3662
         *  [1, 2, 3]
3663
         *  [0 => 1, 1 => 2, 2 => null]
3664
         *  ['a' => 1, 'b' => 2, 0 => 3]
3665
         *
3666
         * Associative keys are preserved, numerical keys are replaced and numerical
3667
         * keys are used for the new elements.
3668
         *
3669
         * @param int $size Total number of elements that should be in the list
3670
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3671
         * @return self<int|string,mixed> New map
3672
         */
3673
        public function pad( int $size, mixed $value = null ) : self
3674
        {
3675
                return new static( array_pad( $this->list(), $size, $value ) );
1✔
3676
        }
3677

3678

3679
        /**
3680
         * Breaks the list of elements into the given number of groups.
3681
         *
3682
         * Examples:
3683
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3684
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3685
         *                return $idx % 3;
3686
         *        } );
3687
         *
3688
         * Results:
3689
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3690
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3691
         *
3692
         * The keys of the original map are preserved in the returned map.
3693
         *
3694
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3695
         * @return self<int|string,mixed> New map
3696
         */
3697
        public function partition( \Closure|int $number ) : self
3698
        {
3699
                $list = $this->list();
3✔
3700

3701
                if( empty( $list ) ) {
3✔
3702
                        return new static();
1✔
3703
                }
3704

3705
                $result = [];
2✔
3706

3707
                if( $number instanceof \Closure )
2✔
3708
                {
3709
                        foreach( $list as $idx => $item ) {
1✔
3710
                                $result[$number( $item, $idx )][$idx] = $item;
1✔
3711
                        }
3712

3713
                        return new static( $result );
1✔
3714
                }
3715

3716
                $start = 0;
1✔
3717
                $size = (int) ceil( count( $list ) / $number );
1✔
3718

3719
                for( $i = 0; $i < $number; $i++ )
1✔
3720
                {
3721
                        $result[] = array_slice( $list, $start, $size, true );
1✔
3722
                        $start += $size;
1✔
3723
                }
3724

3725
                return new static( $result );
1✔
3726
        }
3727

3728

3729
        /**
3730
         * Returns the percentage of all elements passing the test in the map.
3731
         *
3732
         * Examples:
3733
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50 );
3734
         *  Map::from( [] )->percentage( fn( $val, $key ) => true );
3735
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 100 );
3736
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 3 );
3737
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 0 );
3738
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50, -1 );
3739
         *
3740
         * Results:
3741
         * The first line will return "66.67", the second and third one "0.0", the forth
3742
         * one "33.333", the fifth one "33.0" and the last one "70.0" (66 rounded up).
3743
         *
3744
         * @param \Closure $fcn Closure to filter the values in the nested array or object to compute the percentage
3745
         * @param int $precision Number of decimal digits use by the result value
3746
         * @return float Percentage of all elements passing the test in the map
3747
         */
3748
        public function percentage( \Closure $fcn, int $precision = 2 ) : float
3749
        {
3750
                $vals = array_filter( $this->list(), $fcn, ARRAY_FILTER_USE_BOTH );
1✔
3751

3752
                $cnt = count( $this->list() );
1✔
3753
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
1✔
3754
        }
3755

3756

3757
        /**
3758
         * Passes the map to the given callback and return the result.
3759
         *
3760
         * Examples:
3761
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3762
         *      return join( '-', $map->toArray() );
3763
         *  } );
3764
         *
3765
         * Results:
3766
         *  "a-b" will be returned
3767
         *
3768
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3769
         * @return mixed Result returned by the callback
3770
         */
3771
        public function pipe( \Closure $callback ) : mixed
3772
        {
3773
                return $callback( $this );
1✔
3774
        }
3775

3776

3777
        /**
3778
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3779
         *
3780
         * This method is an alias for col(). For performance reasons, col() should
3781
         * be preferred because it uses one method call less than pluck().
3782
         *
3783
         * @param string|null $valuecol Name or path of the value property
3784
         * @param string|null $indexcol Name or path of the index property
3785
         * @return self<int|string,mixed> New map with mapped entries
3786
         * @see col() - Underlying method with same parameters and return value but better performance
3787
         */
3788
        public function pluck( ?string $valuecol = null, ?string $indexcol = null ) : self
3789
        {
3790
                return $this->col( $valuecol, $indexcol );
1✔
3791
        }
3792

3793

3794
        /**
3795
         * Returns and removes the last element from the map.
3796
         *
3797
         * Examples:
3798
         *  Map::from( ['a', 'b'] )->pop();
3799
         *
3800
         * Results:
3801
         *  "b" will be returned and the map only contains ['a'] afterwards
3802
         *
3803
         * @return mixed Last element of the map or null if empty
3804
         */
3805
        public function pop() : mixed
3806
        {
3807
                return array_pop( $this->list() );
2✔
3808
        }
3809

3810

3811
        /**
3812
         * Returns the numerical index of the value.
3813
         *
3814
         * Examples:
3815
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3816
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3817
         *      return $item === 'b';
3818
         *  } );
3819
         *
3820
         * Results:
3821
         * Both examples will return "1" because the value "b" is at the second position
3822
         * and the returned index is zero based so the first item has the index "0".
3823
         *
3824
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3825
         * @return int|null Position of the found value (zero based) or NULL if not found
3826
         */
3827
        public function pos( mixed $value ) : ?int
3828
        {
3829
                $pos = 0;
15✔
3830
                $list = $this->list();
15✔
3831

3832
                if( $value instanceof \Closure )
15✔
3833
                {
3834
                        foreach( $list as $key => $item )
3✔
3835
                        {
3836
                                if( $value( $item, $key ) ) {
3✔
3837
                                        return $pos;
3✔
3838
                                }
3839

3840
                                ++$pos;
3✔
3841
                        }
3842

3843
                        return null;
×
3844
                }
3845

3846
                if( ( $key = array_search( $value, $list, true ) ) !== false
12✔
3847
                        && ( $pos = array_search( $key, array_keys( $list ), true ) ) !== false
12✔
3848
                ) {
3849
                        return $pos;
10✔
3850
                }
3851

3852
                return null;
2✔
3853
        }
3854

3855

3856
        /**
3857
         * Adds a prefix in front of each map entry.
3858
         *
3859
         * By defaul, nested arrays are walked recusively so all entries at all levels are prefixed.
3860
         *
3861
         * Examples:
3862
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3863
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3864
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3865
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3866
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3867
         *  } );
3868
         *
3869
         * Results:
3870
         *  The first example returns ['1-a', '1-b'] while the second one will return
3871
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3872
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3873
         *  the closure will return ['145-a', '147-b'].
3874
         *
3875
         * The keys of the original map are preserved in the returned map.
3876
         *
3877
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3878
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3879
         * @return self<int|string,mixed> Updated map for fluid interface
3880
         */
3881
        public function prefix( \Closure|string $prefix, ?int $depth = null ) : self
3882
        {
3883
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
1✔
3884

3885
                        foreach( $list as $key => $item )
1✔
3886
                        {
3887
                                if( is_array( $item ) ) {
1✔
3888
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
1✔
3889
                                } else {
3890
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
1✔
3891
                                }
3892
                        }
3893

3894
                        return $list;
1✔
3895
                };
1✔
3896

3897
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
1✔
3898
                return $this;
1✔
3899
        }
3900

3901

3902
        /**
3903
         * Pushes an element onto the beginning of the map without returning a new map.
3904
         *
3905
         * This method is an alias for unshift().
3906
         *
3907
         * @param mixed $value Item to add at the beginning
3908
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3909
         * @return self<int|string,mixed> Updated map for fluid interface
3910
         * @see unshift() - Underlying method with same parameters and return value but better performance
3911
         */
3912
        public function prepend( mixed $value, int|string|null $key = null ) : self
3913
        {
3914
                return $this->unshift( $value, $key );
1✔
3915
        }
3916

3917

3918
        /**
3919
         * Returns and removes an element from the map by its key.
3920
         *
3921
         * Examples:
3922
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3923
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3924
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3925
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3926
         *
3927
         * Results:
3928
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3929
         * The second one will return "none" and the map content stays untouched. If you
3930
         * pass an exception as default value, it will throw that exception if the map
3931
         * contains no elements. In the fourth example, a random value generated by the
3932
         * closure function will be returned.
3933
         *
3934
         * @param int|string $key Key to retrieve the value for
3935
         * @param mixed $default Default value if key isn't available
3936
         * @return mixed Value from map or default value
3937
         */
3938
        public function pull( int|string $key, mixed $default = null ) : mixed
3939
        {
3940
                $value = $this->get( $key, $default );
4✔
3941
                unset( $this->list()[$key] );
3✔
3942

3943
                return $value;
3✔
3944
        }
3945

3946

3947
        /**
3948
         * Pushes an element onto the end of the map without returning a new map.
3949
         *
3950
         * Examples:
3951
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3952
         *
3953
         * Results:
3954
         *  ['a', 'b', 'aa']
3955
         *
3956
         * @param mixed $value Value to add to the end
3957
         * @return self<int|string,mixed> Updated map for fluid interface
3958
         */
3959
        public function push( mixed $value ) : self
3960
        {
3961
                $this->list()[] = $value;
3✔
3962
                return $this;
3✔
3963
        }
3964

3965

3966
        /**
3967
         * Sets the given key and value in the map without returning a new map.
3968
         *
3969
         * This method is an alias for set(). For performance reasons, set() should be
3970
         * preferred because it uses one method call less than put().
3971
         *
3972
         * @param int|string $key Key to set the new value for
3973
         * @param mixed $value New element that should be set
3974
         * @return self<int|string,mixed> Updated map for fluid interface
3975
         * @see set() - Underlying method with same parameters and return value but better performance
3976
         */
3977
        public function put( int|string $key, mixed $value ) : self
3978
        {
3979
                return $this->set( $key, $value );
1✔
3980
        }
3981

3982

3983
        /**
3984
         * Returns one or more random element from the map incl. their keys.
3985
         *
3986
         * Examples:
3987
         *  Map::from( [2, 4, 8, 16] )->random();
3988
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3989
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3990
         *
3991
         * Results:
3992
         * The first example will return a map including [0 => 8] or any other value,
3993
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3994
         * and the third example will return a map of the whole list in random order. The
3995
         * less elements are in the map, the less random the order will be, especially if
3996
         * the maximum number of values is high or close to the number of elements.
3997
         *
3998
         * The keys of the original map are preserved in the returned map.
3999
         *
4000
         * @param int $max Maximum number of elements that should be returned
4001
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
4002
         * @throws \InvalidArgumentException If requested number of elements is less than 1
4003
         */
4004
        public function random( int $max = 1 ) : self
4005
        {
4006
                if( $max < 1 ) {
5✔
4007
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
1✔
4008
                }
4009

4010
                $list = $this->list();
4✔
4011

4012
                if( empty( $list ) ) {
4✔
4013
                        return new static();
1✔
4014
                }
4015

4016
                if( ( $num = count( $list ) ) < $max ) {
3✔
4017
                        $max = $num;
1✔
4018
                }
4019

4020
                $keys = array_rand( $list, $max );
3✔
4021

4022
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
3✔
4023
        }
4024

4025

4026
        /**
4027
         * Iteratively reduces the array to a single value using a callback function.
4028
         * Afterwards, the map will be empty.
4029
         *
4030
         * Examples:
4031
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
4032
         *      return $result += $value;
4033
         *  }, 10 );
4034
         *
4035
         * Results:
4036
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
4037
         *
4038
         * @param callable $callback Function with (result, value) parameters and returns result
4039
         * @param mixed $initial Initial value when computing the result
4040
         * @return mixed Value computed by the callback function
4041
         */
4042
        public function reduce( callable $callback, mixed $initial = null ) : mixed
4043
        {
4044
                return array_reduce( $this->list(), $callback, $initial );
1✔
4045
        }
4046

4047

4048
        /**
4049
         * Removes all matched elements and returns a new map.
4050
         *
4051
         * Examples:
4052
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
4053
         *      return $value < 'm';
4054
         *  } );
4055
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
4056
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
4057
         *
4058
         * Results:
4059
         *  [13 => 'm', 30 => 'z']
4060
         *  [2 => 'a', 30 => 'z']
4061
         *  [6 => null]
4062
         *
4063
         * This method is the inverse of the filter() and should return TRUE if the
4064
         * item should be removed from the returned map.
4065
         *
4066
         * If no callback is passed, all values which are NOT empty, null or false will be
4067
         * removed. The keys of the original map are preserved in the returned map.
4068
         *
4069
         * @param Closure|mixed $callback Function with (item, key) parameter which returns TRUE/FALSE
4070
         * @return self<int|string,mixed> New map
4071
         */
4072
        public function reject( mixed $callback = true ) : self
4073
        {
4074
                $result = [];
3✔
4075

4076
                if( $callback instanceof \Closure )
3✔
4077
                {
4078
                        foreach( $this->list() as $key => $value )
1✔
4079
                        {
4080
                                if( !$callback( $value, $key ) ) {
1✔
4081
                                        $result[$key] = $value;
1✔
4082
                                }
4083
                        }
4084
                }
4085
                else
4086
                {
4087
                        foreach( $this->list() as $key => $value )
2✔
4088
                        {
4089
                                if( $value != $callback ) {
2✔
4090
                                        $result[$key] = $value;
2✔
4091
                                }
4092
                        }
4093
                }
4094

4095
                return new static( $result );
3✔
4096
        }
4097

4098

4099
        /**
4100
         * Changes the keys according to the passed function.
4101
         *
4102
         * Examples:
4103
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
4104
         *      return 'key-' . $key;
4105
         *  } );
4106
         *
4107
         * Results:
4108
         *  ['key-a' => 2, 'key-b' => 4]
4109
         *
4110
         * @param callable $callback Function with (value, key) parameters and returns new key
4111
         * @return self<int|string,mixed> New map with new keys and original values
4112
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
4113
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
4114
         */
4115
        public function rekey( callable $callback ) : self
4116
        {
4117
                $list = $this->list();
1✔
4118
                $newKeys = array_map( $callback, $list, array_keys( $list ) );
1✔
4119

4120
                return new static( array_combine( $newKeys, array_values( $list ) ) );
1✔
4121
        }
4122

4123

4124
        /**
4125
         * Removes one or more elements from the map by its keys without returning a new map.
4126
         *
4127
         * Examples:
4128
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
4129
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
4130
         *
4131
         * Results:
4132
         * The first example will result in [2 => 'b'] while the second one resulting
4133
         * in an empty list
4134
         *
4135
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
4136
         * @return self<int|string,mixed> Updated map for fluid interface
4137
         */
4138
        public function remove( iterable|string|int $keys ) : self
4139
        {
4140
                foreach( $this->array( $keys ) as $key ) {
5✔
4141
                        unset( $this->list()[$key] );
5✔
4142
                }
4143

4144
                return $this;
5✔
4145
        }
4146

4147

4148
        /**
4149
         * Replaces elements in the map with the given elements without returning a new map.
4150
         *
4151
         * Examples:
4152
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
4153
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
4154
         *
4155
         * Results:
4156
         *  ['a' => 2, 2 => 'b']
4157
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
4158
         *
4159
         * The method is similar to merge() but it also replaces elements with numeric
4160
         * keys. These would be added by merge() with a new numeric key.
4161
         *
4162
         * The keys are preserved using this method.
4163
         *
4164
         * @param iterable<int|string,mixed> $elements List of elements
4165
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
4166
         * @return self<int|string,mixed> Updated map for fluid interface
4167
         */
4168
        public function replace( iterable $elements, bool $recursive = true ) : self
4169
        {
4170
                if( $recursive ) {
5✔
4171
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
4✔
4172
                } else {
4173
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
1✔
4174
                }
4175

4176
                return $this;
5✔
4177
        }
4178

4179

4180
        /**
4181
         * Returns only the items matching the value (and key) from the map.
4182
         *
4183
         * Examples:
4184
         *  Map::from( ['a', 'b', 'a'] )->restrict( 'a' );
4185
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->restrict( 'test', 'name' );
4186
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->restrict( fn( $v, $k ) => $v['name'] === 'user' );
4187
         *  Map::from( ['a', 'b', 'a'] )->restrict( fn( $v, $k ) => $v === 'a' && $k < 2 );
4188
         *
4189
         * Results:
4190
         *  [0 => 'a', 2 => 'a']
4191
         *  [0 => ['name' => 'test'], 2 => ['name' => 'test']]
4192
         *  [1 => ['name' => 'user']]
4193
         *  [0 => 'a']
4194
         *
4195
         * The keys are preserved in the returned map.
4196
         *
4197
         * @param \Closure|mixed $value Closure with (item, key) parameter or element to test against
4198
         * @param string|int|null $key Key to compare the value to if $value is not a closure
4199
         * @return self<int|string,mixed> New map with matching items only
4200
         */
4201
        public function restrict( mixed $value = null, string|int|null $key = null ) : self
4202
        {
4203
                $filter = $value;
10✔
4204

4205
                if( !( $value instanceof \Closure ) )
10✔
4206
                {
4207
                        if( $key === null )
8✔
4208
                        {
4209
                                $filter = function( $v ) use ( $value ) {
7✔
4210
                                        return $v === $value;
7✔
4211
                                };
7✔
4212
                        }
4213
                        else
4214
                        {
4215
                                $filter = function( $v, $k ) use ( $key, $value ) {
1✔
4216
                                        return ( $v[$key] ?? null ) === $value;
1✔
4217
                                };
1✔
4218
                        }
4219
                }
4220

4221
                return $this->filter( $filter );
10✔
4222
        }
4223

4224

4225
        /**
4226
         * Reverses the element order with keys without returning a new map.
4227
         *
4228
         * Examples:
4229
         *  Map::from( ['a', 'b'] )->reverse();
4230
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
4231
         *
4232
         * Results:
4233
         *  ['b', 'a']
4234
         *  ['last' => 'user', 'name' => 'test']
4235
         *
4236
         * The keys are preserved using this method.
4237
         *
4238
         * @return self<int|string,mixed> Updated map for fluid interface
4239
         * @see reversed() - Reverses the element order in a copy of the map
4240
         */
4241
        public function reverse() : self
4242
        {
4243
                $this->list = array_reverse( $this->list(), true );
4✔
4244
                return $this;
4✔
4245
        }
4246

4247

4248
        /**
4249
         * Reverses the element order in a copy of the map.
4250
         *
4251
         * Examples:
4252
         *  Map::from( ['a', 'b'] )->reversed();
4253
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reversed();
4254
         *
4255
         * Results:
4256
         *  ['b', 'a']
4257
         *  ['last' => 'user', 'name' => 'test']
4258
         *
4259
         * The keys are preserved using this method and a new map is created before reversing the elements.
4260
         * Thus, reverse() should be preferred for performance reasons if possible.
4261
         *
4262
         * @return self<int|string,mixed> New map with a reversed copy of the elements
4263
         * @see reverse() - Reverses the element order with keys without returning a new map
4264
         */
4265
        public function reversed() : self
4266
        {
4267
                return ( clone $this )->reverse();
2✔
4268
        }
4269

4270

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

4301

4302
        /**
4303
         * Sorts a copy of all elements in reverse order using new keys.
4304
         *
4305
         * Examples:
4306
         *  Map::from( ['a' => 1, 'b' => 0] )->rsorted();
4307
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsorted();
4308
         *
4309
         * Results:
4310
         *  [0 => 1, 1 => 0]
4311
         *  [0 => 'b', 1 => 'a']
4312
         *
4313
         * The parameter modifies how the values are compared. Possible parameter values are:
4314
         * - SORT_REGULAR : compare elements normally (don't change types)
4315
         * - SORT_NUMERIC : compare elements numerically
4316
         * - SORT_STRING : compare elements as strings
4317
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4318
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4319
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4320
         *
4321
         * The keys aren't preserved, elements get a new index and a new map is created.
4322
         *
4323
         * @param int $options Sort options for rsort()
4324
         * @return self<int|string,mixed> Updated map for fluid interface
4325
         */
4326
        public function rsorted( int $options = SORT_REGULAR ) : self
4327
        {
4328
                return ( clone $this )->rsort( $options );
1✔
4329
        }
4330

4331

4332
        /**
4333
         * Removes the passed characters from the right of all strings.
4334
         *
4335
         * Examples:
4336
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
4337
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
4338
         *
4339
         * Results:
4340
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
4341
         *
4342
         * @param string $chars List of characters to trim
4343
         * @return self<int|string,mixed> Updated map for fluid interface
4344
         */
4345
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
4346
        {
4347
                foreach( $this->list() as &$entry )
1✔
4348
                {
4349
                        if( is_string( $entry ) ) {
1✔
4350
                                $entry = rtrim( $entry, $chars );
1✔
4351
                        }
4352
                }
4353

4354
                return $this;
1✔
4355
        }
4356

4357

4358
        /**
4359
         * Searches the map for a given value and return the corresponding key if successful.
4360
         *
4361
         * Examples:
4362
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
4363
         *  Map::from( [1, 2, 3] )->search( '2', true );
4364
         *
4365
         * Results:
4366
         * The first example will return 1 (array index) while the second one will
4367
         * return NULL because the types doesn't match (int vs. string)
4368
         *
4369
         * @param mixed $value Item to search for
4370
         * @param bool $strict TRUE if type of the element should be checked too
4371
         * @return int|string|null Key associated to the value or null if not found
4372
         */
4373
        public function search( mixed $value, bool $strict = true ) : int|string|null
4374
        {
4375
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
1✔
4376
                        return $result;
1✔
4377
                }
4378

4379
                return null;
1✔
4380
        }
4381

4382

4383
        /**
4384
         * Sets the seperator for paths to values in multi-dimensional arrays or objects.
4385
         *
4386
         * This method only changes the separator for the current map instance. To
4387
         * change the separator for all maps created afterwards, use the static
4388
         * delimiter() method instead.
4389
         *
4390
         * Examples:
4391
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '.' )->get( 'foo.bar' );
4392
         *
4393
         * Results:
4394
         *  'baz'
4395
         *
4396
         * @param non-empty-string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
4397
         * @return self<int|string,mixed> Same map for fluid interface
4398
         */
4399
        public function sep( string $char ) : self
4400
        {
4401
                $this->sep = $char;
3✔
4402
                return $this;
3✔
4403
        }
4404

4405

4406
        /**
4407
         * Sets an element in the map by key without returning a new map.
4408
         *
4409
         * Examples:
4410
         *  Map::from( ['a'] )->set( 1, 'b' );
4411
         *  Map::from( ['a'] )->set( 0, 'b' );
4412
         *
4413
         * Results:
4414
         *  ['a', 'b']
4415
         *  ['b']
4416
         *
4417
         * @param int|string $key Key to set the new value for
4418
         * @param mixed $value New element that should be set
4419
         * @return self<int|string,mixed> Updated map for fluid interface
4420
         */
4421
        public function set( int|string $key, mixed $value ) : self
4422
        {
4423
                $this->list()[(string) $key] = $value;
5✔
4424
                return $this;
5✔
4425
        }
4426

4427

4428
        /**
4429
         * Returns and removes the first element from the map.
4430
         *
4431
         * Examples:
4432
         *  Map::from( ['a', 'b'] )->shift();
4433
         *  Map::from( [] )->shift();
4434
         *
4435
         * Results:
4436
         * The first example returns "a" and shortens the map to ['b'] only while the
4437
         * second example will return NULL
4438
         *
4439
         * Performance note:
4440
         * The bigger the list, the higher the performance impact because shift()
4441
         * reindexes all existing elements. Usually, it's better to reverse() the list
4442
         * and pop() entries from the list afterwards if a significant number of elements
4443
         * should be removed from the list:
4444
         *
4445
         *  $map->reverse()->pop();
4446
         * instead of
4447
         *  $map->shift( 'a' );
4448
         *
4449
         * @return mixed|null Value from map or null if not found
4450
         */
4451
        public function shift() : mixed
4452
        {
4453
                return array_shift( $this->list() );
1✔
4454
        }
4455

4456

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

4482
                        foreach( $keys as $key ) {
1✔
4483
                                $items[$key] = $list[$key];
1✔
4484
                        }
4485

4486
                        $this->list = $items;
1✔
4487
                }
4488
                else
4489
                {
4490
                        shuffle( $this->list() );
2✔
4491
                }
4492

4493
                return $this;
3✔
4494
        }
4495

4496

4497
        /**
4498
         * Shuffles the elements in a copy of the map.
4499
         *
4500
         * Examples:
4501
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled();
4502
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled( true );
4503
         *
4504
         * Results:
4505
         * The map in the first example will contain "a" and "b" in random order and
4506
         * with new keys assigned. The second call will also return all values in
4507
         * random order but preserves the keys of the original list.
4508
         *
4509
         * @param bool $assoc True to preserve keys, false to assign new keys
4510
         * @return self<int|string,mixed> New map with a shuffled copy of the elements
4511
         * @see shuffle() - Shuffles the elements in the map without returning a new map
4512
         */
4513
        public function shuffled( bool $assoc = false ) : self
4514
        {
4515
                return ( clone $this )->shuffle( $assoc );
1✔
4516
        }
4517

4518

4519
        /**
4520
         * Returns a new map with the given number of items skipped.
4521
         *
4522
         * Examples:
4523
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
4524
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
4525
         *      return $item < 4;
4526
         *  } );
4527
         *
4528
         * Results:
4529
         *  [2 => 3, 3 => 4]
4530
         *  [3 => 4]
4531
         *
4532
         * The keys of the items returned in the new map are the same as in the original one.
4533
         *
4534
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4535
         * @return self<int|string,mixed> New map
4536
         */
4537
        public function skip( \Closure|int $offset ) : self
4538
        {
4539
                if( $offset instanceof \Closure ) {
2✔
4540
                        return new static( array_slice( $this->list(), $this->until( $this->list(), $offset ), null, true ) );
1✔
4541
                }
4542

4543
                return new static( array_slice( $this->list(), (int) $offset, null, true ) );
1✔
4544
        }
4545

4546

4547
        /**
4548
         * Returns a map with the slice from the original map.
4549
         *
4550
         * Examples:
4551
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
4552
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
4553
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
4554
         *
4555
         * Results:
4556
         * The first example will return ['b', 'c'] and the second one ['b'] only.
4557
         * The third example returns ['c'] because the slice starts at the second
4558
         * last value and ends before the last value.
4559
         *
4560
         * The rules for offsets are:
4561
         * - If offset is non-negative, the sequence will start at that offset
4562
         * - If offset is negative, the sequence will start that far from the end
4563
         *
4564
         * Similar for the length:
4565
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4566
         * - If the array is shorter than the length, then only the available array elements will be present
4567
         * - If length is given and is negative then the sequence will stop that many elements from the end
4568
         * - If it is omitted, then the sequence will have everything from offset up until the end
4569
         *
4570
         * The keys of the items returned in the new map are the same as in the original one.
4571
         *
4572
         * @param int $offset Number of elements to start from
4573
         * @param int|null $length Number of elements to return or NULL for no limit
4574
         * @return self<int|string,mixed> New map
4575
         */
4576
        public function slice( int $offset, ?int $length = null ) : self
4577
        {
4578
                return new static( array_slice( $this->list(), $offset, $length, true ) );
6✔
4579
        }
4580

4581

4582
        /**
4583
         * Returns a new map containing sliding windows of the original map.
4584
         *
4585
         * Examples:
4586
         *  Map::from( [1, 2, 3, 4] )->sliding( 2 );
4587
         *  Map::from( [1, 2, 3, 4] )->sliding( 3, 2 );
4588
         *
4589
         * Results:
4590
         * The first example will return [[0 => 1, 1 => 2], [1 => 2, 2 => 3], [2 => 3, 3 => 4]]
4591
         * while the second one will return [[0 => 1, 1 => 2, 2 => 3], [2 => 3, 3 => 4, 4 => 5]]
4592
         *
4593
         * @param int $size Size of each window
4594
         * @param int $step Step size to move the window
4595
         * @return self New map containing arrays for each window
4596
         */
4597
        public function sliding( int $size = 2, int $step = 1 ) : self
4598
        {
4599
                $result = [];
2✔
4600
                $chunks = floor( ( $this->count() - $size ) / $step ) + 1;
2✔
4601

4602
                for( $i = 0; $i < $chunks; $i++ ) {
2✔
4603
                        $result[] = array_slice( $this->list(), $i * $step, $size, true );
2✔
4604
                }
4605

4606
                return new static( $result );
2✔
4607
        }
4608

4609

4610
        /**
4611
         * Returns the matching item, but only if one matching item exists.
4612
         *
4613
         * Examples:
4614
         *  Map::from( ['a', 'b'] )->sole( 'a' );
4615
         *  Map::from( ['a', 'b', 'a'] )->restrict( fn( $v, $k ) => $v === 'a' && $k < 2 );
4616
         *  Map::from( [['name' => 'test'], ['name' => 'user']] )->restrict( fn( $v, $k ) => $v['name'] === 'user' );
4617
         *  Map::from( ['b', 'c'] )->sole( 'a' );
4618
         *  Map::from( ['a', 'b', 'a'] )->sole( 'a' );
4619
         *  Map::from( [['name' => 'test'], ['name' => 'user'], ['name' => 'test']] )->restrict( 'test', 'name' );
4620
         *
4621
         * Results:
4622
         * The first two examples will return "a" while the third one will return [1 => ['name' => 'user']].
4623
         * All other examples throw a LengthException because more than one item matches the test.
4624
         *
4625
         * @param \Closure|mixed $value Closure with (item, key) parameter or element to test against
4626
         * @param string|int|null $key Key to compare the value for if $value is not a closure
4627
         * @return mixed Value from map if exactly one matching item exists
4628
         * @throws \LengthException If no items or more than one item is found
4629
         */
4630
        public function sole( mixed $value = null, string|int|null $key = null ) : mixed
4631
        {
4632
                $items = $this->restrict( $value, $key );
3✔
4633

4634
                if( $items->count() > 1 ) {
3✔
4635
                        throw new \LengthException( 'Multiple items found' );
1✔
4636
                }
4637

4638
                return $items->first( new \LengthException( 'No items found' ) );
2✔
4639
        }
4640

4641

4642
        /**
4643
         * Tests if at least one element passes the test or is part of the map.
4644
         *
4645
         * Examples:
4646
         *  Map::from( ['a', 'b'] )->some( 'a' );
4647
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
4648
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
4649
         *    return $item === 'a';
4650
         *  } );
4651
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
4652
         *  Map::from( ['1', '2'] )->some( [2], true );
4653
         *
4654
         * Results:
4655
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
4656
         *
4657
         * @param \Closure|iterable|mixed $values Closure with (item, key) parameter, element or list of elements to test against
4658
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
4659
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
4660
         */
4661
        public function some( mixed $values, bool $strict = false ) : bool
4662
        {
4663
                $list = $this->list();
6✔
4664

4665
                if( is_iterable( $values ) )
6✔
4666
                {
4667
                        foreach( $values as $entry )
3✔
4668
                        {
4669
                                if( in_array( $entry, $list, $strict ) === true ) {
3✔
4670
                                        return true;
3✔
4671
                                }
4672
                        }
4673

4674
                        return false;
2✔
4675
                }
4676

4677
                if( $values instanceof \Closure )
4✔
4678
                {
4679
                        foreach( $list as $key => $item )
2✔
4680
                        {
4681
                                if( $values( $item, $key ) ) {
2✔
4682
                                        return true;
2✔
4683
                                }
4684
                        }
4685
                }
4686

4687
                return in_array( $values, $list, $strict );
4✔
4688
        }
4689

4690

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

4722

4723
        /**
4724
         * Sorts the elements in a copy of the map using new keys.
4725
         *
4726
         * Examples:
4727
         *  Map::from( ['a' => 1, 'b' => 0] )->sorted();
4728
         *  Map::from( [0 => 'b', 1 => 'a'] )->sorted();
4729
         *
4730
         * Results:
4731
         *  [0 => 0, 1 => 1]
4732
         *  [0 => 'a', 1 => 'b']
4733
         *
4734
         * The parameter modifies how the values are compared. Possible parameter values are:
4735
         * - SORT_REGULAR : compare elements normally (don't change types)
4736
         * - SORT_NUMERIC : compare elements numerically
4737
         * - SORT_STRING : compare elements as strings
4738
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4739
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4740
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4741
         *
4742
         * The keys aren't preserved and elements get a new index and a new map is created before sorting the elements.
4743
         * Thus, sort() should be preferred for performance reasons if possible. A new map is created by calling this method.
4744
         *
4745
         * @param int $options Sort options for PHP sort()
4746
         * @return self<int|string,mixed> New map with a sorted copy of the elements
4747
         * @see sort() - Sorts elements in-place in the original map
4748
         */
4749
        public function sorted( int $options = SORT_REGULAR ) : self
4750
        {
4751
                return ( clone $this )->sort( $options );
2✔
4752
        }
4753

4754

4755
        /**
4756
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4757
         *
4758
         * Examples:
4759
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4760
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4761
         *
4762
         * Results:
4763
         * The first example removes all entries after "a", so only ['a'] will be left
4764
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4765
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4766
         * ['a', 'x', 'y', 'c'] afterwards.
4767
         *
4768
         * The rules for offsets are:
4769
         * - If offset is non-negative, the sequence will start at that offset
4770
         * - If offset is negative, the sequence will start that far from the end
4771
         *
4772
         * Similar for the length:
4773
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4774
         * - If the array is shorter than the length, then only the available array elements will be present
4775
         * - If length is given and is negative then the sequence will stop that many elements from the end
4776
         * - If it is omitted, then the sequence will have everything from offset up until the end
4777
         *
4778
         * Numerical array indexes are NOT preserved.
4779
         *
4780
         * @param int $offset Number of elements to start from
4781
         * @param int|null $length Number of elements to remove, NULL for all
4782
         * @param mixed $replacement List of elements to insert
4783
         * @return self<int|string,mixed> New map
4784
         */
4785
        public function splice( int $offset, ?int $length = null, mixed $replacement = [] ) : self
4786
        {
4787
                // PHP 7.x doesn't allow to pass NULL as replacement
4788
                if( $length === null ) {
5✔
4789
                        $length = count( $this->list() );
2✔
4790
                }
4791

4792
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
5✔
4793
        }
4794

4795

4796
        /**
4797
         * Returns the strings after the passed value.
4798
         *
4799
         * Examples:
4800
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4801
         *  Map::from( ['abc'] )->strAfter( '' );
4802
         *  Map::from( ['abc'] )->strAfter( 'b' );
4803
         *  Map::from( ['abc'] )->strAfter( 'c' );
4804
         *  Map::from( ['abc'] )->strAfter( 'x' );
4805
         *  Map::from( [''] )->strAfter( '' );
4806
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4807
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4808
         *
4809
         * Results:
4810
         *  ['üß']
4811
         *  ['abc']
4812
         *  ['c']
4813
         *  ['']
4814
         *  []
4815
         *  []
4816
         *  ['1', '1', '1']
4817
         *  ['0', '0']
4818
         *
4819
         * All scalar values (bool, int, float, string) will be converted to strings.
4820
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4821
         *
4822
         * @param string $value Character or string to search for
4823
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4824
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4825
         * @return self<int|string,mixed> New map
4826
         */
4827
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4828
        {
4829
                $list = [];
1✔
4830
                $len = mb_strlen( $value );
1✔
4831
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
1✔
4832

4833
                foreach( $this->list() as $key => $entry )
1✔
4834
                {
4835
                        if( is_scalar( $entry ) )
1✔
4836
                        {
4837
                                $pos = null;
1✔
4838
                                $str = (string) $entry;
1✔
4839

4840
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
1✔
4841
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
1✔
4842
                                } elseif( $str !== '' && $pos !== false ) {
1✔
4843
                                        $list[$key] = $str;
1✔
4844
                                }
4845
                        }
4846
                }
4847

4848
                return new static( $list );
1✔
4849
        }
4850

4851

4852
        /**
4853
         * Returns the strings before the passed value.
4854
         *
4855
         * Examples:
4856
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4857
         *  Map::from( ['abc'] )->strBefore( '' );
4858
         *  Map::from( ['abc'] )->strBefore( 'b' );
4859
         *  Map::from( ['abc'] )->strBefore( 'a' );
4860
         *  Map::from( ['abc'] )->strBefore( 'x' );
4861
         *  Map::from( [''] )->strBefore( '' );
4862
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4863
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4864
         *
4865
         * Results:
4866
         *  ['äö']
4867
         *  ['abc']
4868
         *  ['a']
4869
         *  ['']
4870
         *  []
4871
         *  []
4872
         *  ['1', '1', '1']
4873
         *  ['0', '0']
4874
         *
4875
         * All scalar values (bool, int, float, string) will be converted to strings.
4876
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4877
         *
4878
         * @param string $value Character or string to search for
4879
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4880
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4881
         * @return self<int|string,mixed> New map
4882
         */
4883
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4884
        {
4885
                $list = [];
1✔
4886
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
1✔
4887

4888
                foreach( $this->list() as $key => $entry )
1✔
4889
                {
4890
                        if( is_scalar( $entry ) )
1✔
4891
                        {
4892
                                $pos = null;
1✔
4893
                                $str = (string) $entry;
1✔
4894

4895
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
1✔
4896
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
1✔
4897
                                } elseif( $str !== '' && $pos !== false ) {
1✔
4898
                                        $list[$key] = $str;
1✔
4899
                                }
4900
                        }
4901
                }
4902

4903
                return new static( $list );
1✔
4904
        }
4905

4906

4907
        /**
4908
         * Compares the value against all map elements.
4909
         *
4910
         * Examples:
4911
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
4912
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
4913
         *  Map::from( [123, 12.3] )->compare( '12.3' );
4914
         *  Map::from( [false, true] )->compare( '1' );
4915
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
4916
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
4917
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
4918
         *
4919
         * Results:
4920
         * The first four examples return TRUE, the last three examples will return FALSE.
4921
         *
4922
         * All scalar values (bool, float, int and string) are casted to string values before
4923
         * comparing to the given value. Non-scalar values in the map are ignored.
4924
         *
4925
         * @param string $value Value to compare map elements to
4926
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
4927
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
4928
         */
4929
        public function strCompare( string $value, bool $case = true ) : bool
4930
        {
4931
                $fcn = $case ? 'strcmp' : 'strcasecmp';
2✔
4932

4933
                foreach( $this->list() as $item )
2✔
4934
                {
4935
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
2✔
4936
                                return true;
2✔
4937
                        }
4938
                }
4939

4940
                return false;
2✔
4941
        }
4942

4943

4944
        /**
4945
         * Tests if at least one of the passed strings is part of at least one entry.
4946
         *
4947
         * Examples:
4948
         *  Map::from( ['abc'] )->strContains( '' );
4949
         *  Map::from( ['abc'] )->strContains( 'a' );
4950
         *  Map::from( ['abc'] )->strContains( 'bc' );
4951
         *  Map::from( [12345] )->strContains( '23' );
4952
         *  Map::from( [123.4] )->strContains( 23.4 );
4953
         *  Map::from( [12345] )->strContains( false );
4954
         *  Map::from( [12345] )->strContains( true );
4955
         *  Map::from( [false] )->strContains( false );
4956
         *  Map::from( [''] )->strContains( false );
4957
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4958
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4959
         *
4960
         *  Map::from( ['abc'] )->strContains( 'd' );
4961
         *  Map::from( ['abc'] )->strContains( 'cb' );
4962
         *  Map::from( [23456] )->strContains( true );
4963
         *  Map::from( [false] )->strContains( 0 );
4964
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4965
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4966
         *
4967
         * Results:
4968
         * The first eleven examples will return TRUE while the last six will return FALSE.
4969
         *
4970
         * @param array<string>|string $value The string or list of strings to search for in each entry
4971
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4972
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4973
         * @todo 4.0 Add $case parameter at second position
4974
         */
4975
        public function strContains( array|string $value, string $encoding = 'UTF-8' ) : bool
4976
        {
4977
                foreach( $this->list() as $entry )
1✔
4978
                {
4979
                        $entry = (string) $entry;
1✔
4980

4981
                        foreach( (array) $value as $str )
1✔
4982
                        {
4983
                                $str = (string) $str;
1✔
4984

4985
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
1✔
4986
                                        return true;
1✔
4987
                                }
4988
                        }
4989
                }
4990

4991
                return false;
1✔
4992
        }
4993

4994

4995
        /**
4996
         * Tests if all of the entries contains one of the passed strings.
4997
         *
4998
         * Examples:
4999
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
5000
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
5001
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
5002
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
5003
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
5004
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
5005
         *  Map::from( ['', false] )->strContainsAll( false );
5006
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
5007
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
5008
         *
5009
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
5010
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
5011
         *  Map::from( [23456, '123'] )->strContainsAll( true );
5012
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
5013
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
5014
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
5015
         *
5016
         * Results:
5017
         * The first nine examples will return TRUE while the last six will return FALSE.
5018
         *
5019
         * @param array<string>|string $value The string or list of strings to search for in each entry
5020
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5021
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
5022
         * @todo 4.0 Add $case parameter at second position
5023
         */
5024
        public function strContainsAll( array|string $value, string $encoding = 'UTF-8' ) : bool
5025
        {
5026
                $list = [];
1✔
5027

5028
                foreach( $this->list() as $entry )
1✔
5029
                {
5030
                        $entry = (string) $entry;
1✔
5031
                        $list[$entry] = 0;
1✔
5032

5033
                        foreach( (array) $value as $str )
1✔
5034
                        {
5035
                                $str = (string) $str;
1✔
5036

5037
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
1✔
5038
                                        $list[$entry] = 1; break;
1✔
5039
                                }
5040
                        }
5041
                }
5042

5043
                return array_sum( $list ) === count( $list );
1✔
5044
        }
5045

5046

5047
        /**
5048
         * Tests if at least one of the entries ends with one of the passed strings.
5049
         *
5050
         * Examples:
5051
         *  Map::from( ['abc'] )->strEnds( '' );
5052
         *  Map::from( ['abc'] )->strEnds( 'c' );
5053
         *  Map::from( ['abc'] )->strEnds( 'bc' );
5054
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
5055
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
5056
         *  Map::from( ['abc'] )->strEnds( 'a' );
5057
         *  Map::from( ['abc'] )->strEnds( 'cb' );
5058
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
5059
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
5060
         *
5061
         * Results:
5062
         * The first five examples will return TRUE while the last four will return FALSE.
5063
         *
5064
         * @param array<string>|string $value The string or strings to search for in each entry
5065
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5066
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
5067
         * @todo 4.0 Add $case parameter at second position
5068
         */
5069
        public function strEnds( array|string $value, string $encoding = 'UTF-8' ) : bool
5070
        {
5071
                foreach( $this->list() as $entry )
1✔
5072
                {
5073
                        $entry = (string) $entry;
1✔
5074

5075
                        foreach( (array) $value as $str )
1✔
5076
                        {
5077
                                $len = mb_strlen( (string) $str );
1✔
5078

5079
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
1✔
5080
                                        return true;
1✔
5081
                                }
5082
                        }
5083
                }
5084

5085
                return false;
1✔
5086
        }
5087

5088

5089
        /**
5090
         * Tests if all of the entries ends with at least one of the passed strings.
5091
         *
5092
         * Examples:
5093
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
5094
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
5095
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
5096
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
5097
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
5098
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
5099
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
5100
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
5101
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
5102
         *
5103
         * Results:
5104
         * The first five examples will return TRUE while the last four will return FALSE.
5105
         *
5106
         * @param array<string>|string $value The string or strings to search for in each entry
5107
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5108
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
5109
         * @todo 4.0 Add $case parameter at second position
5110
         */
5111
        public function strEndsAll( array|string $value, string $encoding = 'UTF-8' ) : bool
5112
        {
5113
                $list = [];
1✔
5114

5115
                foreach( $this->list() as $entry )
1✔
5116
                {
5117
                        $entry = (string) $entry;
1✔
5118
                        $list[$entry] = 0;
1✔
5119

5120
                        foreach( (array) $value as $str )
1✔
5121
                        {
5122
                                $len = mb_strlen( (string) $str );
1✔
5123

5124
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
1✔
5125
                                        $list[$entry] = 1; break;
1✔
5126
                                }
5127
                        }
5128
                }
5129

5130
                return array_sum( $list ) === count( $list );
1✔
5131
        }
5132

5133

5134
        /**
5135
         * Returns an element by key and casts it to string if possible.
5136
         *
5137
         * Examples:
5138
         *  Map::from( ['a' => true] )->string( 'a' );
5139
         *  Map::from( ['a' => 1] )->string( 'a' );
5140
         *  Map::from( ['a' => 1.1] )->string( 'a' );
5141
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
5142
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
5143
         *  Map::from( [] )->string( 'a', function( $val ) { return 'no'; } );
5144
         *
5145
         *  Map::from( [] )->string( 'b' );
5146
         *  Map::from( ['b' => ''] )->string( 'b' );
5147
         *  Map::from( ['b' => null] )->string( 'b' );
5148
         *  Map::from( ['b' => [true]] )->string( 'b' );
5149
         *  Map::from( ['b' => resource] )->string( 'b' );
5150
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
5151
         *
5152
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
5153
         *
5154
         * Results:
5155
         * The first six examples will return the value as string while the 9th to 12th
5156
         * example returns an empty string. The last example will throw an exception.
5157
         *
5158
         * This does also work for multi-dimensional arrays by passing the keys
5159
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5160
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5161
         * public properties of objects or objects implementing __isset() and __get() methods.
5162
         *
5163
         * @param int|string $key Key or path to the requested item
5164
         * @param \Closure|\Throwable|string $default Default value if key isn't found
5165
         * @return string Value from map or default value
5166
         */
5167
        public function string( int|string $key, \Closure|\Throwable|string $default = '' ) : string
5168
        {
5169
                if( is_scalar( $val = $this->get( $key, $default ) ) ) {
3✔
5170
                        return (string) $val;
2✔
5171
                }
5172

5173
                if( $default instanceof \Closure ) {
1✔
5174
                        return (string) $default( $val );
×
5175
                }
5176

5177
                if( $default instanceof \Throwable ) {
1✔
5178
                        throw $default;
×
5179
                }
5180

5181
                return (string) $default;
1✔
5182
        }
5183

5184

5185
        /**
5186
         * Converts all alphabetic characters in strings to lower case.
5187
         *
5188
         * Examples:
5189
         *  Map::from( ['My String'] )->strLower();
5190
         *  Map::from( ['Τάχιστη'] )->strLower();
5191
         *  Map::from( ['Äpfel', 'Birnen'] )->strLower( 'ISO-8859-1' );
5192
         *  Map::from( [123] )->strLower();
5193
         *  Map::from( [new stdClass] )->strLower();
5194
         *
5195
         * Results:
5196
         * The first example will return ["my string"], the second one ["τάχιστη"] and
5197
         * the third one ["äpfel", "birnen"]. The last two strings will be unchanged.
5198
         *
5199
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5200
         * @return self<int|string,mixed> Updated map for fluid interface
5201
         */
5202
        public function strLower( string $encoding = 'UTF-8' ) : self
5203
        {
5204
                foreach( $this->list() as &$entry )
1✔
5205
                {
5206
                        if( is_string( $entry ) ) {
1✔
5207
                                $entry = mb_strtolower( $entry, $encoding );
1✔
5208
                        }
5209
                }
5210

5211
                return $this;
1✔
5212
        }
5213

5214

5215
        /**
5216
         * Replaces all occurrences of the search string with the replacement string.
5217
         *
5218
         * Examples:
5219
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
5220
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
5221
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
5222
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
5223
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
5224
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
5225
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
5226
         *
5227
         * Restults:
5228
         * ['google.de', 'aimeos.de']
5229
         * ['google.de', 'aimeos.de']
5230
         * ['google.de', 'aimeos']
5231
         * ['google.fr', 'aimeos.de']
5232
         * ['google.de', 'aimeos.de']
5233
         * ['google.de', 'aimeos.de', 123]
5234
         * ['GOOGLE.de', 'AIMEOS.de']
5235
         *
5236
         * If you use an array of strings for search or search/replacement, the order of
5237
         * the strings matters! Each search string found is replaced by the corresponding
5238
         * replacement string at the same position.
5239
         *
5240
         * In case of array parameters and if the number of replacement strings is less
5241
         * than the number of search strings, the search strings with no corresponding
5242
         * replacement string are replaced with empty strings. Replacement strings with
5243
         * no corresponding search string are ignored.
5244
         *
5245
         * An array parameter for the replacements is only allowed if the search parameter
5246
         * is an array of strings too!
5247
         *
5248
         * Because the method replaces from left to right, it might replace a previously
5249
         * inserted value when doing multiple replacements. Entries which are non-string
5250
         * values are left untouched.
5251
         *
5252
         * @param array<string>|string $search String or list of strings to search for
5253
         * @param array<string>|string $replace String or list of strings of replacement strings
5254
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
5255
         * @return self<int|string,mixed> Updated map for fluid interface
5256
         */
5257
        public function strReplace( array|string $search, array|string $replace, bool $case = false ) : self
5258
        {
5259
                $fcn = $case ? 'str_ireplace' : 'str_replace';
1✔
5260

5261
                foreach( $this->list() as &$entry )
1✔
5262
                {
5263
                        if( is_string( $entry ) ) {
1✔
5264
                                $entry = $fcn( $search, $replace, $entry );
1✔
5265
                        }
5266
                }
5267

5268
                return $this;
1✔
5269
        }
5270

5271

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

5300
                        foreach( (array) $value as $str )
1✔
5301
                        {
5302
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
1✔
5303
                                        return true;
1✔
5304
                                }
5305
                        }
5306
                }
5307

5308
                return false;
1✔
5309
        }
5310

5311

5312
        /**
5313
         * Tests if all of the entries starts with one of the passed strings.
5314
         *
5315
         * Examples:
5316
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
5317
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
5318
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
5319
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
5320
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
5321
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
5322
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
5323
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
5324
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
5325
         *
5326
         * Results:
5327
         * The first five examples will return TRUE while the last four will return FALSE.
5328
         *
5329
         * @param array<string>|string $value The string or strings to search for in each entry
5330
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5331
         * @return bool TRUE if one of the entries starts with one of the strings, FALSE if not
5332
         * @todo 4.0 Add $case parameter at second position
5333
         */
5334
        public function strStartsAll( array|string $value, string $encoding = 'UTF-8' ) : bool
5335
        {
5336
                $list = [];
1✔
5337

5338
                foreach( $this->list() as $entry )
1✔
5339
                {
5340
                        $entry = (string) $entry;
1✔
5341
                        $list[$entry] = 0;
1✔
5342

5343
                        foreach( (array) $value as $str )
1✔
5344
                        {
5345
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
1✔
5346
                                        $list[$entry] = 1; break;
1✔
5347
                                }
5348
                        }
5349
                }
5350

5351
                return array_sum( $list ) === count( $list );
1✔
5352
        }
5353

5354

5355
        /**
5356
         * Converts all alphabetic characters in strings to upper case.
5357
         *
5358
         * Examples:
5359
         *  Map::from( ['My String'] )->strUpper();
5360
         *  Map::from( ['τάχιστη'] )->strUpper();
5361
         *  Map::from( ['äpfel', 'birnen'] )->strUpper( 'ISO-8859-1' );
5362
         *  Map::from( [123] )->strUpper();
5363
         *  Map::from( [new stdClass] )->strUpper();
5364
         *
5365
         * Results:
5366
         * The first example will return ["MY STRING"], the second one ["ΤΆΧΙΣΤΗ"] and
5367
         * the third one ["ÄPFEL", "BIRNEN"]. The last two strings will be unchanged.
5368
         *
5369
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5370
         * @return self<int|string,mixed> Updated map for fluid interface
5371
         */
5372
        public function strUpper( string $encoding = 'UTF-8' ) :self
5373
        {
5374
                foreach( $this->list() as &$entry )
1✔
5375
                {
5376
                        if( is_string( $entry ) ) {
1✔
5377
                                $entry = mb_strtoupper( $entry, $encoding );
1✔
5378
                        }
5379
                }
5380

5381
                return $this;
1✔
5382
        }
5383

5384

5385
        /**
5386
         * Adds a suffix at the end of each map entry.
5387
         *
5388
         * By defaul, nested arrays are walked recusively so all entries at all levels are suffixed.
5389
         *
5390
         * Examples:
5391
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
5392
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
5393
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
5394
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
5395
         *      return '-' . ( ord( $item ) + ord( $key ) );
5396
         *  } );
5397
         *
5398
         * Results:
5399
         *  The first example returns ['a-1', 'b-1'] while the second one will return
5400
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
5401
         *  level only so it will return ['a-1', ['b']]. The forth example passing
5402
         *  the closure will return ['a-145', 'b-147'].
5403
         *
5404
         * The keys are preserved using this method.
5405
         *
5406
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
5407
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
5408
         * @return self<int|string,mixed> Updated map for fluid interface
5409
         */
5410
        public function suffix( \Closure|string $suffix, ?int $depth = null ) : self
5411
        {
5412
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
1✔
5413

5414
                        foreach( $list as $key => $item )
1✔
5415
                        {
5416
                                if( is_array( $item ) ) {
1✔
5417
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
1✔
5418
                                } else {
5419
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
1✔
5420
                                }
5421
                        }
5422

5423
                        return $list;
1✔
5424
                };
1✔
5425

5426
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
1✔
5427
                return $this;
1✔
5428
        }
5429

5430

5431
        /**
5432
         * Returns the sum of all integer and float values in the map.
5433
         *
5434
         * Examples:
5435
         *  Map::from( [1, 3, 5] )->sum();
5436
         *  Map::from( [1, 'sum', 5] )->sum();
5437
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
5438
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
5439
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( fn( $val, $key ) => $val['i']['p'] ?? null )
5440
         *  Map::from( [30, 50, 10] )->sum( fn( $val, $key ) => $val < 50 ? $val : null )
5441
         *
5442
         * Results:
5443
         * The first line will return "9", the second one "6", the third one "90"
5444
         * the forth/fifth "80" and the last one "40".
5445
         *
5446
         * Non-numeric values will be removed before calculation.
5447
         *
5448
         * This does also work for multi-dimensional arrays by passing the keys
5449
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5450
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5451
         * public properties of objects or objects implementing __isset() and __get() methods.
5452
         *
5453
         * @param \Closure|string|null $col Closure, key or path to the values in the nested array or object to sum up
5454
         * @return float Sum of all elements or 0 if there are no elements in the map
5455
         */
5456
        public function sum( \Closure|string|null $col = null ) : float
5457
        {
5458
                $list = $this->list();
3✔
5459
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, 'is_numeric' );
3✔
5460

5461
                return array_sum( $vals );
3✔
5462
        }
5463

5464

5465
        /**
5466
         * Returns a new map with the given number of items.
5467
         *
5468
         * The keys of the items returned in the new map are the same as in the original one.
5469
         *
5470
         * Examples:
5471
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
5472
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
5473
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
5474
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
5475
         *      return $item < 2;
5476
         *  } );
5477
         *
5478
         * Results:
5479
         *  [0 => 1, 1 => 2]
5480
         *  [1 => 2, 2 => 3]
5481
         *  [2 => 3, 3 => 4]
5482
         *  [1 => 2, 2 => 3]
5483
         *
5484
         * The keys of the items returned in the new map are the same as in the original one.
5485
         *
5486
         * @param int $size Number of items to return
5487
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5488
         * @return self<int|string,mixed> New map
5489
         */
5490
        public function take( int $size, \Closure|int $offset = 0 ) : self
5491
        {
5492
                $list = $this->list();
4✔
5493

5494
                if( $offset instanceof \Closure ) {
4✔
5495
                        return new static( array_slice( $list, $this->until( $list, $offset ), $size, true ) );
1✔
5496
                }
5497

5498
                return new static( array_slice( $list, (int) $offset, $size, true ) );
3✔
5499
        }
5500

5501

5502
        /**
5503
         * Passes a clone of the map to the given callback.
5504
         *
5505
         * Use it to "tap" into a chain of methods to check the state between two
5506
         * method calls. The original map is not altered by anything done in the
5507
         * callback.
5508
         *
5509
         * Examples:
5510
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
5511
         *    print_r( $map->remove( 0 )->toArray() );
5512
         *  } )->first();
5513
         *
5514
         * Results:
5515
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
5516
         * then prints the items without the first (`[2, 3]`) in the function passed
5517
         * to `tap()` and returns the first item ("1") at the end.
5518
         *
5519
         * @param callable $callback Function receiving ($map) parameter
5520
         * @return self<int|string,mixed> Same map for fluid interface
5521
         */
5522
        public function tap( callable $callback ) : self
5523
        {
5524
                $callback( clone $this );
1✔
5525
                return $this;
1✔
5526
        }
5527

5528

5529
        /**
5530
         * Returns the elements as a plain array.
5531
         *
5532
         * @return array<int|string,mixed> Plain array
5533
         */
5534
        public function to() : array
5535
        {
5536
                return $this->list = $this->array( $this->list );
1✔
5537
        }
5538

5539

5540
        /**
5541
         * Returns the elements as a plain array.
5542
         *
5543
         * @return array<int|string,mixed> Plain array
5544
         */
5545
        public function toArray() : array
5546
        {
5547
                return $this->list = $this->array( $this->list );
248✔
5548
        }
5549

5550

5551
        /**
5552
         * Returns the elements encoded as JSON string.
5553
         *
5554
         * There are several options available to modify the JSON output:
5555
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5556
         * The parameter can be a single JSON_* constant or a bitmask of several
5557
         * constants combine by bitwise OR (|), e.g.:
5558
         *
5559
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5560
         *
5561
         * @param int $options Combination of JSON_* constants
5562
         * @return string|null Array encoded as JSON string or NULL on failure
5563
         */
5564
        public function toJson( int $options = 0 ) : ?string
5565
        {
5566
                $result = json_encode( $this->list(), $options );
2✔
5567
                return $result !== false ? $result : null;
2✔
5568
        }
5569

5570

5571
        /**
5572
         * Reverses the element order in a copy of the map (alias).
5573
         *
5574
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5575
         * preferred because it uses one method call less than toReversed().
5576
         *
5577
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5578
         * @see reversed() - Underlying method with same parameters and return value but better performance
5579
         */
5580
        public function toReversed() : self
5581
        {
5582
                return $this->reversed();
1✔
5583
        }
5584

5585

5586
        /**
5587
         * Sorts the elements in a copy of the map using new keys (alias).
5588
         *
5589
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5590
         * preferred because it uses one method call less than toSorted().
5591
         *
5592
         * @param int $options Sort options for PHP sort()
5593
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5594
         * @see sorted() - Underlying method with same parameters and return value but better performance
5595
         */
5596
        public function toSorted( int $options = SORT_REGULAR ) : self
5597
        {
5598
                return $this->sorted( $options );
1✔
5599
        }
5600

5601

5602
        /**
5603
         * Creates a HTTP query string from the map elements.
5604
         *
5605
         * Examples:
5606
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5607
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5608
         *
5609
         * Results:
5610
         *  a=1&b=2
5611
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5612
         *
5613
         * @return string Parameter string for GET requests
5614
         */
5615
        public function toUrl() : string
5616
        {
5617
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
2✔
5618
        }
5619

5620

5621
        /**
5622
         * Creates new key/value pairs using the passed function and returns a new map for the result.
5623
         *
5624
         * Examples:
5625
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5626
         *      return [$key . '-2' => $value * 2];
5627
         *  } );
5628
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5629
         *      return [$key => $value * 2, $key . $key => $value * 4];
5630
         *  } );
5631
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5632
         *      return $key < 'b' ? [$key => $value * 2] : null;
5633
         *  } );
5634
         *  Map::from( ['la' => 2, 'le' => 4, 'li' => 6] )->transform( function( $value, $key ) {
5635
         *      return [$key[0] => $value * 2];
5636
         *  } );
5637
         *
5638
         * Results:
5639
         *  ['a-2' => 4, 'b-2' => 8]
5640
         *  ['a' => 4, 'aa' => 8, 'b' => 8, 'bb' => 16]
5641
         *  ['a' => 4]
5642
         *  ['l' => 12]
5643
         *
5644
         * If a key is returned twice, the last value will overwrite previous values.
5645
         *
5646
         * @param \Closure $callback Function with (value, key) parameters and returns an array of new key/value pair(s)
5647
         * @return self<int|string,mixed> New map with the new key/value pairs
5648
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
5649
         * @see rekey() - Changes the keys according to the passed function
5650
         */
5651
        public function transform( \Closure $callback ) : self
5652
        {
5653
                $result = [];
4✔
5654

5655
                foreach( $this->list() as $key => $value )
4✔
5656
                {
5657
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
4✔
5658
                                $result[$newkey] = $newval;
4✔
5659
                        }
5660
                }
5661

5662
                return new static( $result );
4✔
5663
        }
5664

5665

5666
        /**
5667
         * Exchanges rows and columns for a two dimensional map.
5668
         *
5669
         * Examples:
5670
         *  Map::from( [
5671
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5672
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5673
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5674
         *  ] )->transpose();
5675
         *
5676
         *  Map::from( [
5677
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5678
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5679
         *    ['name' => 'C', 2020 => 400]
5680
         *  ] );
5681
         *
5682
         * Results:
5683
         *  [
5684
         *    'name' => ['A', 'B', 'C'],
5685
         *    2020 => [200, 300, 400],
5686
         *    2021 => [100, 200, 300],
5687
         *    2022 => [50, 100, 200]
5688
         *  ]
5689
         *
5690
         *  [
5691
         *    'name' => ['A', 'B', 'C'],
5692
         *    2020 => [200, 300, 400],
5693
         *    2021 => [100, 200],
5694
         *    2022 => [50]
5695
         *  ]
5696
         *
5697
         * @return self<int|string,mixed> New map
5698
         */
5699
        public function transpose() : self
5700
        {
5701
                $result = [];
2✔
5702

5703
                foreach( (array) $this->first( [] ) as $key => $col ) {
2✔
5704
                        $result[$key] = array_column( $this->list(), $key );
2✔
5705
                }
5706

5707
                return new static( $result );
2✔
5708
        }
5709

5710

5711
        /**
5712
         * Traverses trees of nested items passing each item to the callback.
5713
         *
5714
         * This does work for nested arrays and objects with public properties or
5715
         * objects implementing __isset() and __get() methods. To build trees
5716
         * of nested items, use the tree() method.
5717
         *
5718
         * Examples:
5719
         *   Map::from( [[
5720
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5721
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5722
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5723
         *     ]
5724
         *   ]] )->traverse();
5725
         *
5726
         *   Map::from( [[
5727
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5728
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5729
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5730
         *     ]
5731
         *   ]] )->traverse( function( $entry, $key, $level, $parent ) {
5732
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
5733
         *   } );
5734
         *
5735
         *   Map::from( [[
5736
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5737
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5738
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5739
         *     ]
5740
         *   ]] )->traverse( function( &$entry, $key, $level, $parent ) {
5741
         *     $entry['path'] = isset( $parent['path'] ) ? $parent['path'] . '/' . $entry['name'] : $entry['name'];
5742
         *     return $entry;
5743
         *   } );
5744
         *
5745
         *   Map::from( [[
5746
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
5747
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
5748
         *     ]
5749
         *   ]] )->traverse( null, 'nodes' );
5750
         *
5751
         * Results:
5752
         *   [
5753
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
5754
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5755
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
5756
         *   ]
5757
         *
5758
         *   ['- n1', '-- n2', '-- n3']
5759
         *
5760
         *   [
5761
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...], 'path' => 'n1'],
5762
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => [], 'path' => 'n1/n2'],
5763
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => [], 'path' => 'n1/n3'],
5764
         *   ]
5765
         *
5766
         *   [
5767
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
5768
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
5769
         *   ]
5770
         *
5771
         * @param \Closure|null $callback Callback with (entry, key, level, $parent) arguments, returns the entry added to result
5772
         * @param string $nestKey Key to the children of each item
5773
         * @return self<int|string,mixed> New map with all items as flat list
5774
         */
5775
        public function traverse( ?\Closure $callback = null, string $nestKey = 'children' ) : self
5776
        {
5777
                $result = [];
5✔
5778
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
5✔
5779

5780
                return map( $result );
5✔
5781
        }
5782

5783

5784
        /**
5785
         * Creates a tree structure from the list items.
5786
         *
5787
         * Use this method to rebuild trees e.g. from database records. To traverse
5788
         * trees, use the traverse() method.
5789
         *
5790
         * Examples:
5791
         *  Map::from( [
5792
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
5793
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
5794
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
5795
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
5796
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
5797
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
5798
         *  ] )->tree( 'id', 'pid' );
5799
         *
5800
         * Results:
5801
         *   [1 => [
5802
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
5803
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
5804
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
5805
         *       ]],
5806
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
5807
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
5808
         *       ]],
5809
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
5810
         *     ]
5811
         *   ]]
5812
         *
5813
         * To build the tree correctly, the items must be in order or at least the
5814
         * nodes of the lower levels must come first. For a tree like this:
5815
         * n1
5816
         * |- n2
5817
         * |  |- n3
5818
         * |- n4
5819
         * |  |- n5
5820
         * |- n6
5821
         *
5822
         * Accepted item order:
5823
         * - in order: n1, n2, n3, n4, n5, n6
5824
         * - lower levels first: n1, n2, n4, n6, n3, n5
5825
         *
5826
         * If your items are unordered, apply usort() first to the map entries, e.g.
5827
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
5828
         *     return $item1['lvl'] <=> $item2['lvl'];
5829
         *   } );
5830
         *
5831
         * @param string $idKey Name of the key with the unique ID of the node
5832
         * @param string $parentKey Name of the key with the ID of the parent node
5833
         * @param string $nestKey Name of the key with will contain the children of the node
5834
         * @return self<int|string,mixed> New map with one or more root tree nodes
5835
         */
5836
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
5837
        {
5838
                $this->list();
1✔
5839
                $trees = $refs = [];
1✔
5840

5841
                foreach( $this->list as &$node )
1✔
5842
                {
5843
                        $node[$nestKey] = [];
1✔
5844
                        $refs[$node[$idKey]] = &$node;
1✔
5845

5846
                        if( $node[$parentKey] ) {
1✔
5847
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
1✔
5848
                        } else {
5849
                                $trees[$node[$idKey]] = &$node;
1✔
5850
                        }
5851
                }
5852

5853
                return map( $trees );
1✔
5854
        }
5855

5856

5857
        /**
5858
         * Removes the passed characters from the left/right of all strings.
5859
         *
5860
         * Examples:
5861
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5862
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5863
         *
5864
         * Results:
5865
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5866
         *
5867
         * @param string $chars List of characters to trim
5868
         * @return self<int|string,mixed> Updated map for fluid interface
5869
         */
5870
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5871
        {
5872
                foreach( $this->list() as &$entry )
1✔
5873
                {
5874
                        if( is_string( $entry ) ) {
1✔
5875
                                $entry = trim( $entry, $chars );
1✔
5876
                        }
5877
                }
5878

5879
                return $this;
1✔
5880
        }
5881

5882

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

5912

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

5941

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

5971

5972
        /**
5973
         * Sorts a copy of the map elements by their keys using a callback.
5974
         *
5975
         * The given callback will be used to compare the keys. The callback must accept
5976
         * two parameters (key A and B) and must return -1 if key A is smaller than
5977
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5978
         * method name and an anonymous function can be passed.
5979
         *
5980
         * Examples:
5981
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5982
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5983
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5984
         *  } );
5985
         *
5986
         * Results:
5987
         *  ['a' => 'b', 'B' => 'a']
5988
         *  ['a' => 'b', 'B' => 'a']
5989
         *
5990
         * The keys are preserved using this method and a new map is created.
5991
         *
5992
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5993
         * @return self<int|string,mixed> Updated map for fluid interface
5994
         */
5995
        public function uksorted( callable $callback ) : self
5996
        {
5997
                return ( clone $this )->uksort( $callback );
1✔
5998
        }
5999

6000

6001
        /**
6002
         * Unflattens the key path/value pairs into a multi-dimensional array.
6003
         *
6004
         * Examples:
6005
         *  Map::from( ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3] )->unflatten();
6006
         *  Map::from( ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3] )->sep( '.' )->unflatten();
6007
         *
6008
         * Results:
6009
         * ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]]
6010
         *
6011
         * This is the inverse method for flatten().
6012
         *
6013
         * @return self<int|string,mixed> New map with multi-dimensional arrays
6014
         */
6015
        public function unflatten() : self
6016
        {
6017
                $result = [];
1✔
6018

6019
                foreach( $this->list() as $key => $value )
1✔
6020
                {
6021
                        $nested = &$result;
1✔
6022
                        $parts = explode( $this->sep, (string) $key );
1✔
6023

6024
                        while( count( $parts ) > 1 ) {
1✔
6025
                                $nested = &$nested[array_shift( $parts )] ?? [];
1✔
6026
                        }
6027

6028
                        $nested[array_shift( $parts )] = $value;
1✔
6029
                }
6030

6031
                return new static( $result );
1✔
6032
        }
6033

6034

6035
        /**
6036
         * Builds a union of the elements and the given elements without overwriting existing ones.
6037
         * Existing keys in the map will not be overwritten
6038
         *
6039
         * Examples:
6040
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
6041
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
6042
         *
6043
         * Results:
6044
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
6045
         * isn't overwritten. In the second example, the result will be a combined
6046
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
6047
         *
6048
         * If list entries should be overwritten,  please use merge() instead!
6049
         * The keys are preserved using this method and no new map is created.
6050
         *
6051
         * @param iterable<int|string,mixed> $elements List of elements
6052
         * @return self<int|string,mixed> Updated map for fluid interface
6053
         */
6054
        public function union( iterable $elements ) : self
6055
        {
6056
                $this->list = $this->list() + $this->array( $elements );
2✔
6057
                return $this;
2✔
6058
        }
6059

6060

6061
        /**
6062
         * Returns only unique elements from the map incl. their keys.
6063
         *
6064
         * Examples:
6065
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
6066
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
6067
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
6068
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( fn( $item, $key ) => $item['i']['p'] )
6069
         *
6070
         * Results:
6071
         * [0 => 'a', 1 => 'b', 3 => 'c']
6072
         * [['p' => 1], ['p' => 2]]
6073
         * [['i' => ['p' => '1']]]
6074
         * [['i' => ['p' => '1']]]
6075
         *
6076
         * Two elements are considered equal if comparing their string representions returns TRUE:
6077
         * (string) $elem1 === (string) $elem2
6078
         *
6079
         * The keys of the elements are only preserved in the new map if no key is passed.
6080
         *
6081
         * @param \Closure|string|null $col Key, path of the nested array or anonymous function with ($item, $key) parameters returning the value for comparison
6082
         * @return self<int|string,mixed> New map
6083
         */
6084
        public function unique( \Closure|string|null $col = null ) : self
6085
        {
6086
                if( $col === null ) {
5✔
6087
                        return new static( array_unique( $this->list() ) );
2✔
6088
                }
6089

6090
                $list = $this->list();
3✔
6091
                $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
3✔
6092

6093
                return new static( array_intersect_key( $list, array_unique( $map ) ) );
3✔
6094
        }
6095

6096

6097
        /**
6098
         * Pushes an element onto the beginning of the map without returning a new map.
6099
         *
6100
         * Examples:
6101
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
6102
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
6103
         *
6104
         * Results:
6105
         *  ['d', 'a', 'b']
6106
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
6107
         *
6108
         * The keys of the elements are only preserved in the new map if no key is passed.
6109
         *
6110
         * Performance note:
6111
         * The bigger the list, the higher the performance impact because unshift()
6112
         * needs to create a new list and copies all existing elements to the new
6113
         * array. Usually, it's better to push() new entries at the end and reverse()
6114
         * the list afterwards:
6115
         *
6116
         *  $map->push( 'a' )->push( 'b' )->reverse();
6117
         * instead of
6118
         *  $map->unshift( 'a' )->unshift( 'b' );
6119
         *
6120
         * @param mixed $value Item to add at the beginning
6121
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
6122
         * @return self<int|string,mixed> Updated map for fluid interface
6123
         */
6124
        public function unshift( mixed $value, int|string|null $key = null ) : self
6125
        {
6126
                if( $key === null ) {
3✔
6127
                        array_unshift( $this->list(), $value );
2✔
6128
                } else {
6129
                        $this->list = [$key => $value] + $this->list();
1✔
6130
                }
6131

6132
                return $this;
3✔
6133
        }
6134

6135

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

6165

6166
        /**
6167
         * Sorts a copy of all elements using a callback using new keys.
6168
         *
6169
         * The given callback will be used to compare the values. The callback must accept
6170
         * two parameters (item A and B) and must return -1 if item A is smaller than
6171
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
6172
         * method name and an anonymous function can be passed.
6173
         *
6174
         * Examples:
6175
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
6176
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
6177
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
6178
         *  } );
6179
         *
6180
         * Results:
6181
         *  [0 => 'a', 1 => 'B']
6182
         *  [0 => 'a', 1 => 'B']
6183
         *
6184
         * The keys aren't preserved, elements get a new index and a new map is created.
6185
         *
6186
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
6187
         * @return self<int|string,mixed> Updated map for fluid interface
6188
         */
6189
        public function usorted( callable $callback ) : self
6190
        {
6191
                return ( clone $this )->usort( $callback );
1✔
6192
        }
6193

6194

6195
        /**
6196
         * Resets the keys and return the values in a new map.
6197
         *
6198
         * Examples:
6199
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
6200
         *
6201
         * Results:
6202
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
6203
         *
6204
         * @return self<int|string,mixed> New map of the values
6205
         */
6206
        public function values() : self
6207
        {
6208
                return new static( array_values( $this->list() ) );
10✔
6209
        }
6210

6211

6212
        /**
6213
         * Applies the given callback to all elements.
6214
         *
6215
         * To change the values of the Map, specify the value parameter as reference
6216
         * (&$value). You can only change the values but not the keys nor the array
6217
         * structure.
6218
         *
6219
         * Examples:
6220
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
6221
         *    $value = strtoupper( $value );
6222
         *  } );
6223
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
6224
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
6225
         *  } );
6226
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
6227
         *    $value = $data[$value] ?? $value;
6228
         *  }, [1 => 'one', 2 => 'two'] );
6229
         *
6230
         * Results:
6231
         * The first example will change the Map elements to:
6232
         *   ['A', 'B', ['C', 'D'], 'E']
6233
         * The output of the second one will be:
6234
         *  ASCII 66 is B
6235
         *  ASCII 97 is a
6236
         * The last example changes the Map elements to:
6237
         *  ['one', 'two', 3]
6238
         *
6239
         * By default, Map elements which are arrays will be traversed recursively.
6240
         * To iterate over the Map elements only, pass FALSE as third parameter.
6241
         *
6242
         * @param callable $callback Function with (item, key, data) parameters
6243
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
6244
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
6245
         * @return self<int|string,mixed> Updated map for fluid interface
6246
         */
6247
        public function walk( callable $callback, mixed $data = null, bool $recursive = true ) : self
6248
        {
6249
                if( $recursive ) {
3✔
6250
                        array_walk_recursive( $this->list(), $callback, $data );
2✔
6251
                } else {
6252
                        array_walk( $this->list(), $callback, $data );
1✔
6253
                }
6254

6255
                return $this;
3✔
6256
        }
6257

6258

6259
        /**
6260
         * Filters the list of elements by a given condition.
6261
         *
6262
         * Examples:
6263
         *  Map::from( [
6264
         *    ['id' => 1, 'type' => 'name'],
6265
         *    ['id' => 2, 'type' => 'short'],
6266
         *  ] )->where( 'type', '==', 'name' );
6267
         *
6268
         *  Map::from( [
6269
         *    ['id' => 3, 'price' => 10],
6270
         *    ['id' => 4, 'price' => 50],
6271
         *  ] )->where( 'price', '>', 20 );
6272
         *
6273
         *  Map::from( [
6274
         *    ['id' => 3, 'price' => 10],
6275
         *    ['id' => 4, 'price' => 50],
6276
         *  ] )->where( 'price', 'in', [10, 25] );
6277
         *
6278
         *  Map::from( [
6279
         *    ['id' => 3, 'price' => 10],
6280
         *    ['id' => 4, 'price' => 50],
6281
         *  ] )->where( 'price', '-', [10, 100] );
6282
         *
6283
         *  Map::from( [
6284
         *    ['item' => ['id' => 3, 'price' => 10]],
6285
         *    ['item' => ['id' => 4, 'price' => 50]],
6286
         *  ] )->where( 'item/price', '>', 30 );
6287
         *
6288
         * Results:
6289
         *  [0 => ['id' => 1, 'type' => 'name']]
6290
         *  [1 => ['id' => 4, 'price' => 50]]
6291
         *  [0 => ['id' => 3, 'price' => 10]]
6292
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
6293
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
6294
         *
6295
         * Available operators are:
6296
         * * '==' : Equal
6297
         * * '===' : Equal and same type
6298
         * * '!=' : Not equal
6299
         * * '!==' : Not equal and same type
6300
         * * '<=' : Smaller than an equal
6301
         * * '>=' : Greater than an equal
6302
         * * '<' : Smaller
6303
         * * '>' : Greater
6304
         * 'in' : Array of value which are in the list of values
6305
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
6306
         *
6307
         * This does also work for multi-dimensional arrays by passing the keys
6308
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
6309
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
6310
         * public properties of objects or objects implementing __isset() and __get() methods.
6311
         *
6312
         * The keys of the original map are preserved in the returned map.
6313
         *
6314
         * @param string $key Key or path of the value in the array or object used for comparison
6315
         * @param string $op Operator used for comparison
6316
         * @param mixed $value Value used for comparison
6317
         * @return self<int|string,mixed> New map for fluid interface
6318
         */
6319
        public function where( string $key, string $op, mixed $value ) : self
6320
        {
6321
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
6✔
6322

6323
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
6✔
6324
                        {
6325
                                switch( $op )
6326
                                {
6327
                                        case '-':
5✔
6328
                                                $list = (array) $value;
1✔
6329
                                                return $val >= current( $list ) && $val <= end( $list );
1✔
6330
                                        case 'in': return in_array( $val, (array) $value );
4✔
6331
                                        case '<': return $val < $value;
3✔
6332
                                        case '>': return $val > $value;
3✔
6333
                                        case '<=': return $val <= $value;
2✔
6334
                                        case '>=': return $val >= $value;
2✔
6335
                                        case '===': return $val === $value;
2✔
6336
                                        case '!==': return $val !== $value;
2✔
6337
                                        case '!=': return $val != $value;
2✔
6338
                                        default: return $val == $value;
2✔
6339
                                }
6340
                        }
6341

6342
                        return false;
1✔
6343
                } );
6✔
6344
        }
6345

6346

6347
        /**
6348
         * Returns a copy of the map with the element at the given index replaced with the given value.
6349
         *
6350
         * Examples:
6351
         *  $m = Map::from( ['a' => 1] );
6352
         *  $m->with( 2, 'b' );
6353
         *  $m->with( 'a', 2 );
6354
         *
6355
         * Results:
6356
         *  ['a' => 1, 2 => 'b']
6357
         *  ['a' => 2]
6358
         *
6359
         * The original map ($m) stays untouched!
6360
         * This method is a shortcut for calling the copy() and set() methods.
6361
         *
6362
         * @param int|string $key Array key to set or replace
6363
         * @param mixed $value New value for the given key
6364
         * @return self<int|string,mixed> New map
6365
         */
6366
        public function with( int|string $key, mixed $value ) : self
6367
        {
6368
                return ( clone $this )->set( $key, $value );
1✔
6369
        }
6370

6371

6372
        /**
6373
         * Merges the values of all arrays at the corresponding index.
6374
         *
6375
         * Examples:
6376
         *  $en = ['one', 'two', 'three'];
6377
         *  $es = ['uno', 'dos', 'tres'];
6378
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
6379
         *
6380
         * Results:
6381
         *  [
6382
         *    [1, 'one', 'uno'],
6383
         *    [2, 'two', 'dos'],
6384
         *    [3, 'three', 'tres'],
6385
         *  ]
6386
         *
6387
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
6388
         * @return self<int|string,mixed> New map of arrays
6389
         */
6390
        public function zip( ...$arrays ) : self
6391
        {
6392
                $args = array_map( function( $items ) {
1✔
6393
                        return $this->array( $items );
1✔
6394
                }, $arrays );
1✔
6395

6396
                return new static( array_map( null, $this->list(), ...$args ) );
1✔
6397
        }
6398

6399

6400
        /**
6401
         * Returns a plain array of the given elements.
6402
         *
6403
         * @param mixed $elements List of elements or single value
6404
         * @return array<int|string,mixed> Plain array
6405
         */
6406
        protected function array( mixed $elements ) : array
6407
        {
6408
                if( is_array( $elements ) ) {
263✔
6409
                        return $elements;
253✔
6410
                }
6411

6412
                if( $elements instanceof \Closure ) {
39✔
6413
                        return (array) $elements();
×
6414
                }
6415

6416
                if( $elements instanceof \Aimeos\Map ) {
39✔
6417
                        return $elements->toArray();
23✔
6418
                }
6419

6420
                if( is_iterable( $elements ) ) {
17✔
6421
                        return iterator_to_array( $elements, true );
3✔
6422
                }
6423

6424
                return $elements !== null ? [$elements] : [];
14✔
6425
        }
6426

6427

6428
        /**
6429
         * Flattens a multi-dimensional array or map into a single level array.
6430
         *
6431
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6432
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
6433
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6434
         */
6435
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
6436
        {
6437
                foreach( $entries as $key => $entry )
5✔
6438
                {
6439
                        if( is_iterable( $entry ) && $depth > 0 ) {
5✔
6440
                                $this->kflatten( $entry, $result, $depth - 1 );
5✔
6441
                        } else {
6442
                                $result[$key] = $entry;
5✔
6443
                        }
6444
                }
6445
        }
6446

6447

6448
        /**
6449
         * Returns a reference to the array of elements
6450
         *
6451
         * @return array<int|string,mixed> Reference to the array of elements
6452
         */
6453
        protected function &list() : array
6454
        {
6455
                if( !is_array( $this->list ) ) {
374✔
6456
                        $this->list = $this->array( $this->list );
×
6457
                }
6458

6459
                return $this->list;
374✔
6460
        }
6461

6462

6463
        /**
6464
         * Returns a closure that retrieves the value for the passed key
6465
         *
6466
         * @param \Closure|string|null $key Closure or key (e.g. "key1/key2/key3") to retrieve the value for
6467
         * @return \Closure Closure that retrieves the value for the passed key
6468
         */
6469
        protected function mapper( \Closure|string|null $key = null ) : \Closure
6470
        {
6471
                if( $key instanceof \Closure ) {
14✔
6472
                        return $key;
6✔
6473
                }
6474

6475
                $parts = $key ? explode( $this->sep, (string) $key ) : [];
8✔
6476

6477
                return function( $item ) use ( $parts ) {
8✔
6478
                        return $this->val( $item, $parts );
8✔
6479
                };
8✔
6480
        }
6481

6482

6483
        /**
6484
         * Flattens a multi-dimensional array or map into a single level array.
6485
         *
6486
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6487
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
6488
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6489
         */
6490
        protected function nflatten( iterable $entries, array &$result, int $depth ) : void
6491
        {
6492
                foreach( $entries as $entry )
5✔
6493
                {
6494
                        if( is_iterable( $entry ) && $depth > 0 ) {
5✔
6495
                                $this->nflatten( $entry, $result, $depth - 1 );
4✔
6496
                        } else {
6497
                                $result[] = $entry;
5✔
6498
                        }
6499
                }
6500
        }
6501

6502

6503
        /**
6504
         * Flattens a multi-dimensional array or map into an array with joined keys.
6505
         *
6506
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6507
         * @param array<int|string,mixed> $result Will contain joined key/value pairs from the multi-dimensional arrays afterwards
6508
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6509
         * @param string $path Path prefix of the current key
6510
         */
6511
        protected function rflatten( iterable $entries, array &$result, int $depth, string $path = '' ) : void
6512
        {
6513
                foreach( $entries as $key => $entry )
1✔
6514
                {
6515
                        if( is_iterable( $entry ) && $depth > 0 ) {
1✔
6516
                                $this->rflatten( $entry, $result, $depth - 1, $path . $key . $this->sep );
1✔
6517
                        } else {
6518
                                $result[$path . $key] = $entry;
1✔
6519
                        }
6520
                }
6521
        }
6522

6523

6524
        /**
6525
         * Returns the position of the first element that doesn't match the condition
6526
         *
6527
         * @param iterable<int|string,mixed> $list List of elements to check
6528
         * @param \Closure $callback Closure with ($item, $key) arguments to check the condition
6529
         * @return int Position of the first element that doesn't match the condition
6530
         */
6531
        protected function until( iterable $list, \Closure $callback ) : int
6532
        {
6533
                $idx = 0;
2✔
6534

6535
                foreach( $list as $key => $item )
2✔
6536
                {
6537
                        if( !$callback( $item, $key ) ) {
2✔
6538
                                break;
2✔
6539
                        }
6540

6541
                        ++$idx;
2✔
6542
                }
6543

6544
                return $idx;
2✔
6545
        }
6546

6547

6548
        /**
6549
         * Returns a configuration value from an array.
6550
         *
6551
         * @param array<mixed>|object $entry The array or object to look at
6552
         * @param array<string> $parts Path parts to look for inside the array or object
6553
         * @return mixed Found value or null if no value is available
6554
         */
6555
        protected function val( mixed $entry, array $parts ) : mixed
6556
        {
6557
                foreach( $parts as $part )
47✔
6558
                {
6559
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
45✔
6560
                                $entry = $entry[$part];
26✔
6561
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
27✔
6562
                                $entry = $entry->{$part};
2✔
6563
                        } else {
6564
                                return null;
25✔
6565
                        }
6566
                }
6567

6568
                return $entry;
29✔
6569
        }
6570

6571

6572
        /**
6573
         * Visits each entry, calls the callback and returns the items in the result argument
6574
         *
6575
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
6576
         * @param array<mixed> $result Numerically indexed list of all visited entries
6577
         * @param int $level Current depth of the nodes in the tree
6578
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
6579
         * @param string $nestKey Key to the children of each entry
6580
         * @param array<mixed>|object|null $parent Parent entry
6581
         */
6582
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, array|object|null $parent = null ) : void
6583
        {
6584
                foreach( $entries as $key => $entry )
5✔
6585
                {
6586
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
5✔
6587

6588
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
5✔
6589
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
4✔
6590
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
1✔
6591
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
1✔
6592
                        }
6593
                }
6594
        }
6595
}
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