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

aimeos / map / 13719225567

07 Mar 2025 10:57AM UTC coverage: 97.672% (+0.05%) from 97.619%
13719225567

push

github

aimeos
Implemented flatten() method

14 of 14 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

797 of 816 relevant lines covered (97.67%)

49.33 hits per line

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

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

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

8

9
namespace Aimeos;
10

11

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

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

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

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

41

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

56

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

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

82

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

118
                $result = [];
16✔
119

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

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

130

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

141

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

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

168
                return $old;
8✔
169
        }
170

171

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

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

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

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

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

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

228

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

252

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

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

280

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

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

318

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

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

348

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

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

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

389

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

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

421

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

432

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

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

463
                return false;
8✔
464
        }
465

466

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

501

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

535

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

570

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

604

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

631

632
        /**
633
         * Returns the average of all integer and float values in the map.
634
         *
635
         * Examples:
636
         *  Map::from( [1, 3, 5] )->avg();
637
         *  Map::from( [1, null, 5] )->avg();
638
         *  Map::from( [1, 'sum', 5] )->avg();
639
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->avg( 'p' );
640
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->avg( 'i/p' );
641
         *  Map::from( [['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( $col = null ) : float
659
        {
660
                $list = $this->list();
24✔
661
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, 'is_numeric' );
24✔
662

663
                return !empty( $vals ) ? array_sum( $vals ) / count( $vals ) : 0;
24✔
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( $value ) : self
690
        {
691
                return new static( array_slice( $this->list(), 0, $this->pos( $value ), true ) );
32✔
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() { 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 mixed $default Default value if key isn't found (will be casted to bool)
728
         * @return bool Value from map or default value
729
         */
730
        public function bool( $key, $default = false ) : bool
731
        {
732
                return (bool) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
733
        }
734

735

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

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

769
                return new static( $result );
8✔
770
        }
771

772

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

813
                return $this;
8✔
814
        }
815

816

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

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

847

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

859

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

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

884
                return new static( $list );
8✔
885
        }
886

887

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

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

933
                $list = [];
24✔
934

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

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

946
                return new static( $list );
24✔
947
        }
948

949

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

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

989

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

1007

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

1023

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

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

1046
                return $this;
16✔
1047
        }
1048

1049

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

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

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

1090

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

1104

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

1115

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

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

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

1160

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

1186

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

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

1225

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

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

1266

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

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

1306

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

1338

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

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

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

1378

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

1404
                return $this;
16✔
1405
        }
1406

1407

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

1427

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

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

1456

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

1484
                return true;
8✔
1485
        }
1486

1487

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

1509

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

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

1542

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

1573
                if( !empty( $list ) )
40✔
1574
                {
1575
                        if( $reverse )
40✔
1576
                        {
1577
                                $value = end( $list );
16✔
1578
                                $key = key( $list );
16✔
1579

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

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

1601
                if( $default instanceof \Closure ) {
24✔
1602
                        return $default();
8✔
1603
                }
1604

1605
                if( $default instanceof \Throwable ) {
16✔
1606
                        throw $default;
8✔
1607
                }
1608

1609
                return $default;
8✔
1610
        }
1611

1612

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

1646
                if( !empty( $list ) )
40✔
1647
                {
1648
                        if( $reverse )
16✔
1649
                        {
1650
                                $value = end( $list );
8✔
1651
                                $key = key( $list );
8✔
1652

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

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

1674
                if( $default instanceof \Closure ) {
24✔
1675
                        return $default();
8✔
1676
                }
1677

1678
                if( $default instanceof \Throwable ) {
16✔
1679
                        throw $default;
8✔
1680
                }
1681

1682
                return $default;
8✔
1683
        }
1684

1685

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

1712
                if( $default instanceof \Closure ) {
40✔
1713
                        return $default();
8✔
1714
                }
1715

1716
                if( $default instanceof \Throwable ) {
32✔
1717
                        throw $default;
8✔
1718
                }
1719

1720
                return $default;
24✔
1721
        }
1722

1723

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

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

1753
                if( $key !== null ) {
40✔
1754
                        return $key;
8✔
1755
                }
1756

1757
                if( $default instanceof \Closure ) {
32✔
1758
                        return $default();
8✔
1759
                }
1760

1761
                if( $default instanceof \Throwable ) {
24✔
1762
                        throw $default;
8✔
1763
                }
1764

1765
                return $default;
16✔
1766
        }
1767

1768

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

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

1807

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

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

1838

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

1855

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

1896

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

1926
                if( array_key_exists( $key, $list ) ) {
184✔
1927
                        return $list[$key];
48✔
1928
                }
1929

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

1934
                if( $default instanceof \Closure ) {
144✔
1935
                        return $default();
48✔
1936
                }
1937

1938
                if( $default instanceof \Throwable ) {
96✔
1939
                        throw $default;
48✔
1940
                }
1941

1942
                return $default;
48✔
1943
        }
1944

1945

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

1959

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

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

2001
                return new static( $result );
24✔
2002
        }
2003

2004

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

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

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

2075
                return new static( $result );
32✔
2076
        }
2077

2078

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

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

2117
                return true;
24✔
2118
        }
2119

2120

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

2178
                if( $condition ) {
64✔
2179
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
40✔
2180
                } elseif( $else ) {
24✔
2181
                        return static::from( $else( $this, $condition ) );
24✔
2182
                }
2183

2184
                return $this;
×
2185
        }
2186

2187

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

2230

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

2268

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

2301
                                return false;
10✔
2302
                        }
2303
                }
2304

2305
                return true;
8✔
2306
        }
2307

2308

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

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

2339
                return true;
8✔
2340
        }
2341

2342

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

2359

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

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

2388
                                ++$pos;
8✔
2389
                        }
2390

2391
                        return null;
8✔
2392
                }
2393

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

2398

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

2423
                return $this;
24✔
2424
        }
2425

2426

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

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

2464
                return $this;
40✔
2465
        }
2466

2467

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

2492
                return $this;
24✔
2493
        }
2494

2495

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

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

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

2545
                return false;
8✔
2546
        }
2547

2548

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

2589

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

2624
                if( $callback ) {
16✔
2625
                        return new static( array_uintersect( $list, $elements, $callback ) );
8✔
2626
                }
2627

2628
                return new static( array_intersect( $list, $elements ) );
8✔
2629
        }
2630

2631

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

2666
                if( $callback ) {
40✔
2667
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
8✔
2668
                }
2669

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

2673

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

2709
                if( $callback ) {
24✔
2710
                        return new static( array_intersect_ukey( $this->list(), $elements, $callback ) );
8✔
2711
                }
2712

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

2716

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

2736
                if( $strict ) {
24✔
2737
                        return $this->list() === $list;
16✔
2738
                }
2739

2740
                return $this->list() == $list;
8✔
2741
        }
2742

2743

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

2763

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

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

2791
                return true;
8✔
2792
        }
2793

2794

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

2832
                return true;
8✔
2833
        }
2834

2835

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

2858
                return true;
8✔
2859
        }
2860

2861

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

2890
                return true;
8✔
2891
        }
2892

2893

2894
        /**
2895
         * Determines if all entries are string values.
2896
         *
2897
         * Examples:
2898
         *  Map::from( ['abc'] )->isString();
2899
         *  Map::from( [] )->isString();
2900
         *  Map::from( [1] )->isString();
2901
         *  Map::from( [1.1] )->isString();
2902
         *  Map::from( [true, false] )->isString();
2903
         *  Map::from( [new stdClass] )->isString();
2904
         *  Map::from( [#resource] )->isString();
2905
         *  Map::from( [null] )->isString();
2906
         *  Map::from( [[1]] )->isString();
2907
         *
2908
         * Results:
2909
         *  The first two examples return TRUE while the others return FALSE
2910
         *
2911
         * @return bool TRUE if all map entries are string values, FALSE if not
2912
         */
2913
        public function isString() : bool
2914
        {
2915
                foreach( $this->list() as $val )
8✔
2916
                {
2917
                        if( !is_string( $val ) ) {
8✔
2918
                                return false;
8✔
2919
                        }
2920
                }
2921

2922
                return true;
8✔
2923
        }
2924

2925

2926
        /**
2927
         * Concatenates the string representation of all elements.
2928
         *
2929
         * Objects that implement __toString() does also work, otherwise (and in case
2930
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2931
         * empty strings.
2932
         *
2933
         * Examples:
2934
         *  Map::from( ['a', 'b', false] )->join();
2935
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
2936
         *
2937
         * Results:
2938
         * The first example will return "ab" while the second one will return "a-b--"
2939
         *
2940
         * @param string $glue Character or string added between elements
2941
         * @return string String of concatenated map elements
2942
         */
2943
        public function join( string $glue = '' ) : string
2944
        {
2945
                return implode( $glue, $this->list() );
8✔
2946
        }
2947

2948

2949
        /**
2950
         * Specifies the data which should be serialized to JSON by json_encode().
2951
         *
2952
         * Examples:
2953
         *   json_encode( Map::from( ['a', 'b'] ) );
2954
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
2955
         *
2956
         * Results:
2957
         *   ["a", "b"]
2958
         *   {"a":0,"b":1}
2959
         *
2960
         * @return array<int|string,mixed> Data to serialize to JSON
2961
         */
2962
        #[\ReturnTypeWillChange]
2963
        public function jsonSerialize()
2964
        {
2965
                return $this->list = $this->array( $this->list );
8✔
2966
        }
2967

2968

2969
        /**
2970
         * Returns the keys of the all elements in a new map object.
2971
         *
2972
         * Examples:
2973
         *  Map::from( ['a', 'b'] );
2974
         *  Map::from( ['a' => 0, 'b' => 1] );
2975
         *
2976
         * Results:
2977
         * The first example returns a map containing [0, 1] while the second one will
2978
         * return a map with ['a', 'b'].
2979
         *
2980
         * @return self<int|string,mixed> New map
2981
         */
2982
        public function keys() : self
2983
        {
2984
                return new static( array_keys( $this->list() ) );
8✔
2985
        }
2986

2987

2988
        /**
2989
         * Sorts the elements by their keys in reverse order.
2990
         *
2991
         * Examples:
2992
         *  Map::from( ['b' => 0, 'a' => 1] )->krsort();
2993
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsort();
2994
         *
2995
         * Results:
2996
         *  ['a' => 1, 'b' => 0]
2997
         *  [0 => 'b', 1 => 'a']
2998
         *
2999
         * The parameter modifies how the keys are compared. Possible values are:
3000
         * - SORT_REGULAR : compare elements normally (don't change types)
3001
         * - SORT_NUMERIC : compare elements numerically
3002
         * - SORT_STRING : compare elements as strings
3003
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3004
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3005
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3006
         *
3007
         * The keys are preserved using this method and no new map is created.
3008
         *
3009
         * @param int $options Sort options for krsort()
3010
         * @return self<int|string,mixed> Updated map for fluid interface
3011
         */
3012
        public function krsort( int $options = SORT_REGULAR ) : self
3013
        {
3014
                krsort( $this->list(), $options );
24✔
3015
                return $this;
24✔
3016
        }
3017

3018

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

3048

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

3079

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

3109

3110
        /**
3111
         * Returns the last element from the map.
3112
         *
3113
         * Examples:
3114
         *  Map::from( ['a', 'b'] )->last();
3115
         *  Map::from( [] )->last( 'x' );
3116
         *  Map::from( [] )->last( new \Exception( 'error' ) );
3117
         *  Map::from( [] )->last( function() { return rand(); } );
3118
         *
3119
         * Results:
3120
         * The first example will return 'b' and the second one 'x'. The third example
3121
         * will throw the exception passed if the map contains no elements. In the
3122
         * fourth example, a random value generated by the closure function will be
3123
         * returned.
3124
         *
3125
         * Using this method doesn't affect the internal array pointer.
3126
         *
3127
         * @param mixed $default Default value or exception if the map contains no elements
3128
         * @return mixed Last value of map, (generated) default value or an exception
3129
         */
3130
        public function last( $default = null )
3131
        {
3132
                if( !empty( $this->list() ) ) {
48✔
3133
                        return current( array_slice( $this->list(), -1, 1 ) );
16✔
3134
                }
3135

3136
                if( $default instanceof \Closure ) {
32✔
3137
                        return $default();
8✔
3138
                }
3139

3140
                if( $default instanceof \Throwable ) {
24✔
3141
                        throw $default;
8✔
3142
                }
3143

3144
                return $default;
16✔
3145
        }
3146

3147

3148
        /**
3149
         * Returns the last key from the map.
3150
         *
3151
         * Examples:
3152
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
3153
         *  Map::from( [] )->lastKey( 'x' );
3154
         *  Map::from( [] )->lastKey( new \Exception( 'error' ) );
3155
         *  Map::from( [] )->lastKey( function() { return rand(); } );
3156
         *
3157
         * Results:
3158
         * The first example will return 'a' and the second one 'x', the third one will throw
3159
         * an exception and the last one will return a random value.
3160
         *
3161
         * Using this method doesn't affect the internal array pointer.
3162
         *
3163
         * @param mixed $default Default value, closure or exception if the map contains no elements
3164
         * @return mixed Last key of map, (generated) default value or an exception
3165
         */
3166
        public function lastKey( $default = null )
3167
        {
3168
                $list = $this->list();
40✔
3169

3170
                // PHP 7.x compatibility
3171
                if( function_exists( 'array_key_last' ) ) {
40✔
3172
                        $key = array_key_last( $list );
40✔
3173
                } else {
3174
                        $key = key( array_slice( $list, -1, 1, true ) );
×
3175
                }
3176

3177
                if( $key !== null ) {
40✔
3178
                        return $key;
8✔
3179
                }
3180

3181
                if( $default instanceof \Closure ) {
32✔
3182
                        return $default();
8✔
3183
                }
3184

3185
                if( $default instanceof \Throwable ) {
24✔
3186
                        throw $default;
8✔
3187
                }
3188

3189
                return $default;
16✔
3190
        }
3191

3192

3193
        /**
3194
         * Removes the passed characters from the left of all strings.
3195
         *
3196
         * Examples:
3197
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
3198
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
3199
         *
3200
         * Results:
3201
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
3202
         *
3203
         * @param string $chars List of characters to trim
3204
         * @return self<int|string,mixed> Updated map for fluid interface
3205
         */
3206
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
3207
        {
3208
                foreach( $this->list() as &$entry )
8✔
3209
                {
3210
                        if( is_string( $entry ) ) {
8✔
3211
                                $entry = ltrim( $entry, $chars );
8✔
3212
                        }
3213
                }
3214

3215
                return $this;
8✔
3216
        }
3217

3218

3219
        /**
3220
         * Maps new values to the existing keys using the passed function and returns a new map for the result.
3221
         *
3222
         * Examples:
3223
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
3224
         *      return $value * 2;
3225
         *  } );
3226
         *
3227
         * Results:
3228
         *  ['a' => 4, 'b' => 8]
3229
         *
3230
         * The keys are preserved using this method.
3231
         *
3232
         * @param callable $callback Function with (value, key) parameters and returns computed result
3233
         * @return self<int|string,mixed> New map with the original keys and the computed values
3234
         * @see rekey() - Changes the keys according to the passed function
3235
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3236
         */
3237
        public function map( callable $callback ) : self
3238
        {
3239
                $list = $this->list();
8✔
3240
                $keys = array_keys( $list );
8✔
3241
                $map = array_map( $callback, array_values( $list ), $keys );
8✔
3242

3243
                return new static( array_combine( $keys, $map ) );
8✔
3244
        }
3245

3246

3247
        /**
3248
         * Returns the maximum value of all elements.
3249
         *
3250
         * Examples:
3251
         *  Map::from( [1, 3, 2, 5, 4] )->max()
3252
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
3253
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
3254
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
3255
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( fn( $val, $key ) => $val['i']['p'] ?? null )
3256
         *  Map::from( [50, 10, 30] )->max( fn( $val, $key ) => $key > 0 ? $val : null )
3257
         *
3258
         * Results:
3259
         * The first line will return "5", the second one "foo" and the third to fitfh
3260
         * one return 50 while the last one will return 30.
3261
         *
3262
         * NULL values are removed before the comparison. If there are no values or all
3263
         * values are NULL, NULL is returned.
3264
         *
3265
         * This does also work for multi-dimensional arrays by passing the keys
3266
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3267
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3268
         * public properties of objects or objects implementing __isset() and __get() methods.
3269
         *
3270
         * Be careful comparing elements of different types because this can have
3271
         * unpredictable results due to the PHP comparison rules:
3272
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3273
         *
3274
         * @param Closure|string|null $col Closure, key or path to the value of the nested array or object
3275
         * @return mixed Maximum value or NULL if there are no elements in the map
3276
         */
3277
        public function max( $col = null )
3278
        {
3279
                $list = $this->list();
32✔
3280
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list );
32✔
3281

3282
                return !empty( $vals ) ? max( $vals ) : null;
32✔
3283
        }
3284

3285

3286
        /**
3287
         * Merges the map with the given elements without returning a new map.
3288
         *
3289
         * Elements with the same non-numeric keys will be overwritten, elements
3290
         * with the same numeric keys will be added.
3291
         *
3292
         * Examples:
3293
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
3294
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
3295
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
3296
         *
3297
         * Results:
3298
         *  ['a', 'b', 'b', 'c']
3299
         *  ['a' => 1, 'b' => 4, 'c' => 6]
3300
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
3301
         *
3302
         * The method is similar to replace() but doesn't replace elements with
3303
         * the same numeric keys. If you want to be sure that all passed elements
3304
         * are added without replacing existing ones, use concat() instead.
3305
         *
3306
         * The keys are preserved using this method.
3307
         *
3308
         * @param iterable<int|string,mixed> $elements List of elements
3309
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
3310
         * @return self<int|string,mixed> Updated map for fluid interface
3311
         */
3312
        public function merge( iterable $elements, bool $recursive = false ) : self
3313
        {
3314
                if( $recursive ) {
24✔
3315
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
8✔
3316
                } else {
3317
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
16✔
3318
                }
3319

3320
                return $this;
24✔
3321
        }
3322

3323

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

3359
                return !empty( $vals ) ? min( $vals ) : null;
32✔
3360
        }
3361

3362

3363
        /**
3364
         * Tests if none of the elements are part of the map.
3365
         *
3366
         * Examples:
3367
         *  Map::from( ['a', 'b'] )->none( 'x' );
3368
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
3369
         *  Map::from( ['1', '2'] )->none( 2, true );
3370
         *  Map::from( ['a', 'b'] )->none( 'a' );
3371
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
3372
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
3373
         *
3374
         * Results:
3375
         * The first three examples will return TRUE while the other ones will return FALSE
3376
         *
3377
         * @param mixed|array $element Element or elements to search for in the map
3378
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3379
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
3380
         */
3381
        public function none( $element, bool $strict = false ) : bool
3382
        {
3383
                $list = $this->list();
8✔
3384

3385
                if( !is_array( $element ) ) {
8✔
3386
                        return !in_array( $element, $list, $strict );
8✔
3387
                };
3388

3389
                foreach( $element as $entry )
8✔
3390
                {
3391
                        if( in_array( $entry, $list, $strict ) === true ) {
8✔
3392
                                return false;
8✔
3393
                        }
3394
                }
3395

3396
                return true;
8✔
3397
        }
3398

3399

3400
        /**
3401
         * Returns every nth element from the map.
3402
         *
3403
         * Examples:
3404
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
3405
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
3406
         *
3407
         * Results:
3408
         *  ['a', 'c', 'e']
3409
         *  ['b', 'd', 'f']
3410
         *
3411
         * @param int $step Step width
3412
         * @param int $offset Number of element to start from (0-based)
3413
         * @return self<int|string,mixed> New map
3414
         */
3415
        public function nth( int $step, int $offset = 0 ) : self
3416
        {
3417
                if( $step < 1 ) {
24✔
3418
                        throw new \InvalidArgumentException( 'Step width must be greater than zero' );
8✔
3419
                }
3420

3421
                if( $step === 1 ) {
16✔
3422
                        return clone $this;
8✔
3423
                }
3424

3425
                $result = [];
8✔
3426
                $list = $this->list();
8✔
3427

3428
                while( !empty( $pair = array_slice( $list, $offset, 1, true ) ) )
8✔
3429
                {
3430
                        $result += $pair;
8✔
3431
                        $offset += $step;
8✔
3432
                }
3433

3434
                return new static( $result );
8✔
3435
        }
3436

3437

3438
        /**
3439
         * Determines if an element exists at an offset.
3440
         *
3441
         * Examples:
3442
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3443
         *  isset( $map['b'] );
3444
         *  isset( $map['c'] );
3445
         *  isset( $map['d'] );
3446
         *
3447
         * Results:
3448
         *  The first isset() will return TRUE while the second and third one will return FALSE
3449
         *
3450
         * @param int|string $key Key to check for
3451
         * @return bool TRUE if key exists, FALSE if not
3452
         */
3453
        public function offsetExists( $key ) : bool
3454
        {
3455
                return isset( $this->list()[$key] );
56✔
3456
        }
3457

3458

3459
        /**
3460
         * Returns an element at a given offset.
3461
         *
3462
         * Examples:
3463
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3464
         *  $map['b'];
3465
         *
3466
         * Results:
3467
         *  $map['b'] will return 3
3468
         *
3469
         * @param int|string $key Key to return the element for
3470
         * @return mixed Value associated to the given key
3471
         */
3472
        #[\ReturnTypeWillChange]
3473
        public function offsetGet( $key )
3474
        {
3475
                return $this->list()[$key] ?? null;
40✔
3476
        }
3477

3478

3479
        /**
3480
         * Sets the element at a given offset.
3481
         *
3482
         * Examples:
3483
         *  $map = Map::from( ['a' => 1] );
3484
         *  $map['b'] = 2;
3485
         *  $map[0] = 4;
3486
         *
3487
         * Results:
3488
         *  ['a' => 1, 'b' => 2, 0 => 4]
3489
         *
3490
         * @param int|string|null $key Key to set the element for or NULL to append value
3491
         * @param mixed $value New value set for the key
3492
         */
3493
        public function offsetSet( $key, $value ) : void
3494
        {
3495
                if( $key !== null ) {
24✔
3496
                        $this->list()[$key] = $value;
16✔
3497
                } else {
3498
                        $this->list()[] = $value;
16✔
3499
                }
3500
        }
6✔
3501

3502

3503
        /**
3504
         * Unsets the element at a given offset.
3505
         *
3506
         * Examples:
3507
         *  $map = Map::from( ['a' => 1] );
3508
         *  unset( $map['a'] );
3509
         *
3510
         * Results:
3511
         *  The map will be empty
3512
         *
3513
         * @param int|string $key Key for unsetting the item
3514
         */
3515
        public function offsetUnset( $key ) : void
3516
        {
3517
                unset( $this->list()[$key] );
16✔
3518
        }
4✔
3519

3520

3521
        /**
3522
         * Returns a new map with only those elements specified by the given keys.
3523
         *
3524
         * Examples:
3525
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3526
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3527
         *
3528
         * Results:
3529
         *  ['a' => 1]
3530
         *  [0 => 'b', 1 => 'c']
3531
         *
3532
         * The keys are preserved using this method.
3533
         *
3534
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3535
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3536
         */
3537
        public function only( $keys ) : self
3538
        {
3539
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
8✔
3540
        }
3541

3542

3543
        /**
3544
         * Returns a new map with elements ordered by the passed keys.
3545
         *
3546
         * If there are less keys passed than available in the map, the remaining
3547
         * elements are removed. Otherwise, if keys are passed that are not in the
3548
         * map, they will be also available in the returned map but their value is
3549
         * NULL.
3550
         *
3551
         * Examples:
3552
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3553
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3554
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3555
         *
3556
         * Results:
3557
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3558
         *  [0 => 'b', 1 => 'c', 2 => null]
3559
         *  [0 => 'b', 1 => 'c']
3560
         *
3561
         * The keys are preserved using this method.
3562
         *
3563
         * @param iterable<mixed> $keys Keys of the elements in the required order
3564
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3565
         */
3566
        public function order( iterable $keys ) : self
3567
        {
3568
                $result = [];
8✔
3569
                $list = $this->list();
8✔
3570

3571
                foreach( $keys as $key ) {
8✔
3572
                        $result[$key] = $list[$key] ?? null;
8✔
3573
                }
3574

3575
                return new static( $result );
8✔
3576
        }
3577

3578

3579
        /**
3580
         * Fill up to the specified length with the given value
3581
         *
3582
         * In case the given number is smaller than the number of element that are
3583
         * already in the list, the map is unchanged. If the size is positive, the
3584
         * new elements are padded on the right, if it's negative then the elements
3585
         * are padded on the left.
3586
         *
3587
         * Examples:
3588
         *  Map::from( [1, 2, 3] )->pad( 5 );
3589
         *  Map::from( [1, 2, 3] )->pad( -5 );
3590
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3591
         *  Map::from( [1, 2, 3] )->pad( 2 );
3592
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3593
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3594
         *
3595
         * Results:
3596
         *  [1, 2, 3, null, null]
3597
         *  [null, null, 1, 2, 3]
3598
         *  [1, 2, 3, '0', '0']
3599
         *  [1, 2, 3]
3600
         *  [0 => 1, 1 => 2, 2 => null]
3601
         *  ['a' => 1, 'b' => 2, 0 => 3]
3602
         *
3603
         * Associative keys are preserved, numerical keys are replaced and numerical
3604
         * keys are used for the new elements.
3605
         *
3606
         * @param int $size Total number of elements that should be in the list
3607
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3608
         * @return self<int|string,mixed> New map
3609
         */
3610
        public function pad( int $size, $value = null ) : self
3611
        {
3612
                return new static( array_pad( $this->list(), $size, $value ) );
8✔
3613
        }
3614

3615

3616
        /**
3617
         * Breaks the list of elements into the given number of groups.
3618
         *
3619
         * Examples:
3620
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3621
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3622
         *                return $idx % 3;
3623
         *        } );
3624
         *
3625
         * Results:
3626
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3627
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3628
         *
3629
         * The keys of the original map are preserved in the returned map.
3630
         *
3631
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3632
         * @return self<int|string,mixed> New map
3633
         */
3634
        public function partition( $number ) : self
3635
        {
3636
                $list = $this->list();
32✔
3637

3638
                if( empty( $list ) ) {
32✔
3639
                        return new static();
8✔
3640
                }
3641

3642
                $result = [];
24✔
3643

3644
                if( $number instanceof \Closure )
24✔
3645
                {
3646
                        foreach( $list as $idx => $item ) {
8✔
3647
                                $result[$number( $item, $idx )][$idx] = $item;
8✔
3648
                        }
3649

3650
                        return new static( $result );
8✔
3651
                }
3652

3653
                if( is_int( $number ) )
16✔
3654
                {
3655
                        $start = 0;
8✔
3656
                        $size = (int) ceil( count( $list ) / $number );
8✔
3657

3658
                        for( $i = 0; $i < $number; $i++ )
8✔
3659
                        {
3660
                                $result[] = array_slice( $list, $start, $size, true );
8✔
3661
                                $start += $size;
8✔
3662
                        }
3663

3664
                        return new static( $result );
8✔
3665
                }
3666

3667
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
8✔
3668
        }
3669

3670

3671
        /**
3672
         * Returns the percentage of all elements passing the test in the map.
3673
         *
3674
         * Examples:
3675
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50 );
3676
         *  Map::from( [] )->percentage( fn( $val, $key ) => true );
3677
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 100 );
3678
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 3 );
3679
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 0 );
3680
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50, -1 );
3681
         *
3682
         * Results:
3683
         * The first line will return "66.67", the second and third one "0.0", the forth
3684
         * one "33.333", the fifth one "33.0" and the last one "70.0" (66 rounded up).
3685
         *
3686
         * @param Closure $fcn Closure to filter the values in the nested array or object to compute the percentage
3687
         * @param int $precision Number of decimal digits use by the result value
3688
         * @return float Percentage of all elements passing the test in the map
3689
         */
3690
        public function percentage( \Closure $fcn, int $precision = 2 ) : float
3691
        {
3692
                $vals = array_filter( $this->list(), $fcn, ARRAY_FILTER_USE_BOTH );
8✔
3693

3694
                $cnt = count( $this->list() );
8✔
3695
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
8✔
3696
        }
3697

3698

3699
        /**
3700
         * Passes the map to the given callback and return the result.
3701
         *
3702
         * Examples:
3703
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3704
         *      return join( '-', $map->toArray() );
3705
         *  } );
3706
         *
3707
         * Results:
3708
         *  "a-b" will be returned
3709
         *
3710
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3711
         * @return mixed Result returned by the callback
3712
         */
3713
        public function pipe( \Closure $callback )
3714
        {
3715
                return $callback( $this );
8✔
3716
        }
3717

3718

3719
        /**
3720
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3721
         *
3722
         * This method is an alias for col(). For performance reasons, col() should
3723
         * be preferred because it uses one method call less than pluck().
3724
         *
3725
         * @param string|null $valuecol Name or path of the value property
3726
         * @param string|null $indexcol Name or path of the index property
3727
         * @return self<int|string,mixed> New map with mapped entries
3728
         * @see col() - Underlying method with same parameters and return value but better performance
3729
         */
3730
        public function pluck( ?string $valuecol = null, ?string $indexcol = null ) : self
3731
        {
3732
                return $this->col( $valuecol, $indexcol );
8✔
3733
        }
3734

3735

3736
        /**
3737
         * Returns and removes the last element from the map.
3738
         *
3739
         * Examples:
3740
         *  Map::from( ['a', 'b'] )->pop();
3741
         *
3742
         * Results:
3743
         *  "b" will be returned and the map only contains ['a'] afterwards
3744
         *
3745
         * @return mixed Last element of the map or null if empty
3746
         */
3747
        public function pop()
3748
        {
3749
                return array_pop( $this->list() );
16✔
3750
        }
3751

3752

3753
        /**
3754
         * Returns the numerical index of the value.
3755
         *
3756
         * Examples:
3757
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3758
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3759
         *      return $item === 'b';
3760
         *  } );
3761
         *
3762
         * Results:
3763
         * Both examples will return "1" because the value "b" is at the second position
3764
         * and the returned index is zero based so the first item has the index "0".
3765
         *
3766
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3767
         * @return int|null Position of the found value (zero based) or NULL if not found
3768
         */
3769
        public function pos( $value ) : ?int
3770
        {
3771
                $pos = 0;
120✔
3772
                $list = $this->list();
120✔
3773

3774
                if( $value instanceof \Closure )
120✔
3775
                {
3776
                        foreach( $list as $key => $item )
24✔
3777
                        {
3778
                                if( $value( $item, $key ) ) {
24✔
3779
                                        return $pos;
24✔
3780
                                }
3781

3782
                                ++$pos;
24✔
3783
                        }
3784

3785
                        return null;
×
3786
                }
3787

3788
                if( ( $key = array_search( $value, $list, true ) ) !== false
96✔
3789
                        && ( $pos = array_search( $key, array_keys( $list ), true ) ) !== false
96✔
3790
                ) {
3791
                        return $pos;
80✔
3792
                }
3793

3794
                return null;
16✔
3795
        }
3796

3797

3798
        /**
3799
         * Adds a prefix in front of each map entry.
3800
         *
3801
         * By defaul, nested arrays are walked recusively so all entries at all levels are prefixed.
3802
         *
3803
         * Examples:
3804
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3805
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3806
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3807
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3808
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3809
         *  } );
3810
         *
3811
         * Results:
3812
         *  The first example returns ['1-a', '1-b'] while the second one will return
3813
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3814
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3815
         *  the closure will return ['145-a', '147-b'].
3816
         *
3817
         * The keys of the original map are preserved in the returned map.
3818
         *
3819
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3820
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3821
         * @return self<int|string,mixed> Updated map for fluid interface
3822
         */
3823
        public function prefix( $prefix, ?int $depth = null ) : self
3824
        {
3825
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
6✔
3826

3827
                        foreach( $list as $key => $item )
8✔
3828
                        {
3829
                                if( is_array( $item ) ) {
8✔
3830
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
8✔
3831
                                } else {
3832
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
8✔
3833
                                }
3834
                        }
3835

3836
                        return $list;
8✔
3837
                };
8✔
3838

3839
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
8✔
3840
                return $this;
8✔
3841
        }
3842

3843

3844
        /**
3845
         * Pushes an element onto the beginning of the map without returning a new map.
3846
         *
3847
         * This method is an alias for unshift().
3848
         *
3849
         * @param mixed $value Item to add at the beginning
3850
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3851
         * @return self<int|string,mixed> Updated map for fluid interface
3852
         * @see unshift() - Underlying method with same parameters and return value but better performance
3853
         */
3854
        public function prepend( $value, $key = null ) : self
3855
        {
3856
                return $this->unshift( $value, $key );
8✔
3857
        }
3858

3859

3860
        /**
3861
         * Returns and removes an element from the map by its key.
3862
         *
3863
         * Examples:
3864
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3865
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3866
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3867
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3868
         *
3869
         * Results:
3870
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3871
         * The second one will return "none" and the map content stays untouched. If you
3872
         * pass an exception as default value, it will throw that exception if the map
3873
         * contains no elements. In the fourth example, a random value generated by the
3874
         * closure function will be returned.
3875
         *
3876
         * @param int|string $key Key to retrieve the value for
3877
         * @param mixed $default Default value if key isn't available
3878
         * @return mixed Value from map or default value
3879
         */
3880
        public function pull( $key, $default = null )
3881
        {
3882
                $value = $this->get( $key, $default );
32✔
3883
                unset( $this->list()[$key] );
24✔
3884

3885
                return $value;
24✔
3886
        }
3887

3888

3889
        /**
3890
         * Pushes an element onto the end of the map without returning a new map.
3891
         *
3892
         * Examples:
3893
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3894
         *
3895
         * Results:
3896
         *  ['a', 'b', 'aa']
3897
         *
3898
         * @param mixed $value Value to add to the end
3899
         * @return self<int|string,mixed> Updated map for fluid interface
3900
         */
3901
        public function push( $value ) : self
3902
        {
3903
                $this->list()[] = $value;
24✔
3904
                return $this;
24✔
3905
        }
3906

3907

3908
        /**
3909
         * Sets the given key and value in the map without returning a new map.
3910
         *
3911
         * This method is an alias for set(). For performance reasons, set() should be
3912
         * preferred because it uses one method call less than put().
3913
         *
3914
         * @param int|string $key Key to set the new value for
3915
         * @param mixed $value New element that should be set
3916
         * @return self<int|string,mixed> Updated map for fluid interface
3917
         * @see set() - Underlying method with same parameters and return value but better performance
3918
         */
3919
        public function put( $key, $value ) : self
3920
        {
3921
                return $this->set( $key, $value );
8✔
3922
        }
3923

3924

3925
        /**
3926
         * Returns one or more random element from the map incl. their keys.
3927
         *
3928
         * Examples:
3929
         *  Map::from( [2, 4, 8, 16] )->random();
3930
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3931
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3932
         *
3933
         * Results:
3934
         * The first example will return a map including [0 => 8] or any other value,
3935
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3936
         * and the third example will return a map of the whole list in random order. The
3937
         * less elements are in the map, the less random the order will be, especially if
3938
         * the maximum number of values is high or close to the number of elements.
3939
         *
3940
         * The keys of the original map are preserved in the returned map.
3941
         *
3942
         * @param int $max Maximum number of elements that should be returned
3943
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
3944
         * @throws \InvalidArgumentException If requested number of elements is less than 1
3945
         */
3946
        public function random( int $max = 1 ) : self
3947
        {
3948
                if( $max < 1 ) {
40✔
3949
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
8✔
3950
                }
3951

3952
                $list = $this->list();
32✔
3953

3954
                if( empty( $list ) ) {
32✔
3955
                        return new static();
8✔
3956
                }
3957

3958
                if( ( $num = count( $list ) ) < $max ) {
24✔
3959
                        $max = $num;
8✔
3960
                }
3961

3962
                $keys = array_rand( $list, $max );
24✔
3963

3964
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
24✔
3965
        }
3966

3967

3968
        /**
3969
         * Iteratively reduces the array to a single value using a callback function.
3970
         * Afterwards, the map will be empty.
3971
         *
3972
         * Examples:
3973
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
3974
         *      return $result += $value;
3975
         *  }, 10 );
3976
         *
3977
         * Results:
3978
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
3979
         *
3980
         * @param callable $callback Function with (result, value) parameters and returns result
3981
         * @param mixed $initial Initial value when computing the result
3982
         * @return mixed Value computed by the callback function
3983
         */
3984
        public function reduce( callable $callback, $initial = null )
3985
        {
3986
                return array_reduce( $this->list(), $callback, $initial );
8✔
3987
        }
3988

3989

3990
        /**
3991
         * Removes all matched elements and returns a new map.
3992
         *
3993
         * Examples:
3994
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
3995
         *      return $value < 'm';
3996
         *  } );
3997
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
3998
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
3999
         *
4000
         * Results:
4001
         *  [13 => 'm', 30 => 'z']
4002
         *  [2 => 'a', 30 => 'z']
4003
         *  [6 => null]
4004
         *
4005
         * This method is the inverse of the filter() and should return TRUE if the
4006
         * item should be removed from the returned map.
4007
         *
4008
         * If no callback is passed, all values which are NOT empty, null or false will be
4009
         * removed. The keys of the original map are preserved in the returned map.
4010
         *
4011
         * @param Closure|mixed $callback Function with (item, key) parameter which returns TRUE/FALSE
4012
         * @return self<int|string,mixed> New map
4013
         */
4014
        public function reject( $callback = true ) : self
4015
        {
4016
                $result = [];
24✔
4017

4018
                if( $callback instanceof \Closure )
24✔
4019
                {
4020
                        foreach( $this->list() as $key => $value )
8✔
4021
                        {
4022
                                if( !$callback( $value, $key ) ) {
8✔
4023
                                        $result[$key] = $value;
8✔
4024
                                }
4025
                        }
4026
                }
4027
                else
4028
                {
4029
                        foreach( $this->list() as $key => $value )
16✔
4030
                        {
4031
                                if( $value != $callback ) {
16✔
4032
                                        $result[$key] = $value;
16✔
4033
                                }
4034
                        }
4035
                }
4036

4037
                return new static( $result );
24✔
4038
        }
4039

4040

4041
        /**
4042
         * Changes the keys according to the passed function.
4043
         *
4044
         * Examples:
4045
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
4046
         *      return 'key-' . $key;
4047
         *  } );
4048
         *
4049
         * Results:
4050
         *  ['key-a' => 2, 'key-b' => 4]
4051
         *
4052
         * @param callable $callback Function with (value, key) parameters and returns new key
4053
         * @return self<int|string,mixed> New map with new keys and original values
4054
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
4055
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
4056
         */
4057
        public function rekey( callable $callback ) : self
4058
        {
4059
                $list = $this->list();
8✔
4060
                $newKeys = array_map( $callback, $list, array_keys( $list ) );
8✔
4061

4062
                return new static( array_combine( $newKeys, array_values( $list ) ) );
8✔
4063
        }
4064

4065

4066
        /**
4067
         * Removes one or more elements from the map by its keys without returning a new map.
4068
         *
4069
         * Examples:
4070
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
4071
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
4072
         *
4073
         * Results:
4074
         * The first example will result in [2 => 'b'] while the second one resulting
4075
         * in an empty list
4076
         *
4077
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
4078
         * @return self<int|string,mixed> Updated map for fluid interface
4079
         */
4080
        public function remove( $keys ) : self
4081
        {
4082
                foreach( $this->array( $keys ) as $key ) {
40✔
4083
                        unset( $this->list()[$key] );
40✔
4084
                }
4085

4086
                return $this;
40✔
4087
        }
4088

4089

4090
        /**
4091
         * Replaces elements in the map with the given elements without returning a new map.
4092
         *
4093
         * Examples:
4094
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
4095
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
4096
         *
4097
         * Results:
4098
         *  ['a' => 2, 2 => 'b']
4099
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
4100
         *
4101
         * The method is similar to merge() but it also replaces elements with numeric
4102
         * keys. These would be added by merge() with a new numeric key.
4103
         *
4104
         * The keys are preserved using this method.
4105
         *
4106
         * @param iterable<int|string,mixed> $elements List of elements
4107
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
4108
         * @return self<int|string,mixed> Updated map for fluid interface
4109
         */
4110
        public function replace( iterable $elements, bool $recursive = true ) : self
4111
        {
4112
                if( $recursive ) {
40✔
4113
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
32✔
4114
                } else {
4115
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
8✔
4116
                }
4117

4118
                return $this;
40✔
4119
        }
4120

4121

4122
        /**
4123
         * Reverses the element order with keys without returning a new map.
4124
         *
4125
         * Examples:
4126
         *  Map::from( ['a', 'b'] )->reverse();
4127
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
4128
         *
4129
         * Results:
4130
         *  ['b', 'a']
4131
         *  ['last' => 'user', 'name' => 'test']
4132
         *
4133
         * The keys are preserved using this method.
4134
         *
4135
         * @return self<int|string,mixed> Updated map for fluid interface
4136
         * @see reversed() - Reverses the element order in a copy of the map
4137
         */
4138
        public function reverse() : self
4139
        {
4140
                $this->list = array_reverse( $this->list(), true );
32✔
4141
                return $this;
32✔
4142
        }
4143

4144

4145
        /**
4146
         * Reverses the element order in a copy of the map.
4147
         *
4148
         * Examples:
4149
         *  Map::from( ['a', 'b'] )->reversed();
4150
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reversed();
4151
         *
4152
         * Results:
4153
         *  ['b', 'a']
4154
         *  ['last' => 'user', 'name' => 'test']
4155
         *
4156
         * The keys are preserved using this method and a new map is created before reversing the elements.
4157
         * Thus, reverse() should be preferred for performance reasons if possible.
4158
         *
4159
         * @return self<int|string,mixed> New map with a reversed copy of the elements
4160
         * @see reverse() - Reverses the element order with keys without returning a new map
4161
         */
4162
        public function reversed() : self
4163
        {
4164
                return ( clone $this )->reverse();
16✔
4165
        }
4166

4167

4168
        /**
4169
         * Sorts all elements in reverse order using new keys.
4170
         *
4171
         * Examples:
4172
         *  Map::from( ['a' => 1, 'b' => 0] )->rsort();
4173
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsort();
4174
         *
4175
         * Results:
4176
         *  [0 => 1, 1 => 0]
4177
         *  [0 => 'b', 1 => 'a']
4178
         *
4179
         * The parameter modifies how the values are compared. Possible parameter values are:
4180
         * - SORT_REGULAR : compare elements normally (don't change types)
4181
         * - SORT_NUMERIC : compare elements numerically
4182
         * - SORT_STRING : compare elements as strings
4183
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4184
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4185
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4186
         *
4187
         * The keys aren't preserved and elements get a new index. No new map is created
4188
         *
4189
         * @param int $options Sort options for rsort()
4190
         * @return self<int|string,mixed> Updated map for fluid interface
4191
         */
4192
        public function rsort( int $options = SORT_REGULAR ) : self
4193
        {
4194
                rsort( $this->list(), $options );
24✔
4195
                return $this;
24✔
4196
        }
4197

4198

4199
        /**
4200
         * Sorts a copy of all elements in reverse order using new keys.
4201
         *
4202
         * Examples:
4203
         *  Map::from( ['a' => 1, 'b' => 0] )->rsorted();
4204
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsorted();
4205
         *
4206
         * Results:
4207
         *  [0 => 1, 1 => 0]
4208
         *  [0 => 'b', 1 => 'a']
4209
         *
4210
         * The parameter modifies how the values are compared. Possible parameter values are:
4211
         * - SORT_REGULAR : compare elements normally (don't change types)
4212
         * - SORT_NUMERIC : compare elements numerically
4213
         * - SORT_STRING : compare elements as strings
4214
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4215
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4216
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4217
         *
4218
         * The keys aren't preserved, elements get a new index and a new map is created.
4219
         *
4220
         * @param int $options Sort options for rsort()
4221
         * @return self<int|string,mixed> Updated map for fluid interface
4222
         */
4223
        public function rsorted( int $options = SORT_REGULAR ) : self
4224
        {
4225
                return ( clone $this )->rsort( $options );
8✔
4226
        }
4227

4228

4229
        /**
4230
         * Removes the passed characters from the right of all strings.
4231
         *
4232
         * Examples:
4233
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
4234
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
4235
         *
4236
         * Results:
4237
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
4238
         *
4239
         * @param string $chars List of characters to trim
4240
         * @return self<int|string,mixed> Updated map for fluid interface
4241
         */
4242
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
4243
        {
4244
                foreach( $this->list() as &$entry )
8✔
4245
                {
4246
                        if( is_string( $entry ) ) {
8✔
4247
                                $entry = rtrim( $entry, $chars );
8✔
4248
                        }
4249
                }
4250

4251
                return $this;
8✔
4252
        }
4253

4254

4255
        /**
4256
         * Searches the map for a given value and return the corresponding key if successful.
4257
         *
4258
         * Examples:
4259
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
4260
         *  Map::from( [1, 2, 3] )->search( '2', true );
4261
         *
4262
         * Results:
4263
         * The first example will return 1 (array index) while the second one will
4264
         * return NULL because the types doesn't match (int vs. string)
4265
         *
4266
         * @param mixed $value Item to search for
4267
         * @param bool $strict TRUE if type of the element should be checked too
4268
         * @return int|string|null Key associated to the value or null if not found
4269
         */
4270
        public function search( $value, $strict = true )
4271
        {
4272
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
8✔
4273
                        return $result;
8✔
4274
                }
4275

4276
                return null;
8✔
4277
        }
4278

4279

4280
        /**
4281
         * Sets the seperator for paths to values in multi-dimensional arrays or objects.
4282
         *
4283
         * This method only changes the separator for the current map instance. To
4284
         * change the separator for all maps created afterwards, use the static
4285
         * delimiter() method instead.
4286
         *
4287
         * Examples:
4288
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '.' )->get( 'foo.bar' );
4289
         *
4290
         * Results:
4291
         *  'baz'
4292
         *
4293
         * @param string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
4294
         * @return self<int|string,mixed> Same map for fluid interface
4295
         */
4296
        public function sep( string $char ) : self
4297
        {
4298
                $this->sep = $char;
24✔
4299
                return $this;
24✔
4300
        }
4301

4302

4303
        /**
4304
         * Sets an element in the map by key without returning a new map.
4305
         *
4306
         * Examples:
4307
         *  Map::from( ['a'] )->set( 1, 'b' );
4308
         *  Map::from( ['a'] )->set( 0, 'b' );
4309
         *
4310
         * Results:
4311
         *  ['a', 'b']
4312
         *  ['b']
4313
         *
4314
         * @param int|string $key Key to set the new value for
4315
         * @param mixed $value New element that should be set
4316
         * @return self<int|string,mixed> Updated map for fluid interface
4317
         */
4318
        public function set( $key, $value ) : self
4319
        {
4320
                $this->list()[(string) $key] = $value;
40✔
4321
                return $this;
40✔
4322
        }
4323

4324

4325
        /**
4326
         * Returns and removes the first element from the map.
4327
         *
4328
         * Examples:
4329
         *  Map::from( ['a', 'b'] )->shift();
4330
         *  Map::from( [] )->shift();
4331
         *
4332
         * Results:
4333
         * The first example returns "a" and shortens the map to ['b'] only while the
4334
         * second example will return NULL
4335
         *
4336
         * Performance note:
4337
         * The bigger the list, the higher the performance impact because shift()
4338
         * reindexes all existing elements. Usually, it's better to reverse() the list
4339
         * and pop() entries from the list afterwards if a significant number of elements
4340
         * should be removed from the list:
4341
         *
4342
         *  $map->reverse()->pop();
4343
         * instead of
4344
         *  $map->shift( 'a' );
4345
         *
4346
         * @return mixed|null Value from map or null if not found
4347
         */
4348
        public function shift()
4349
        {
4350
                return array_shift( $this->list() );
8✔
4351
        }
4352

4353

4354
        /**
4355
         * Shuffles the elements in the map without returning a new map.
4356
         *
4357
         * Examples:
4358
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle();
4359
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle( true );
4360
         *
4361
         * Results:
4362
         * The map in the first example will contain "a" and "b" in random order and
4363
         * with new keys assigned. The second call will also return all values in
4364
         * random order but preserves the keys of the original list.
4365
         *
4366
         * @param bool $assoc True to preserve keys, false to assign new keys
4367
         * @return self<int|string,mixed> Updated map for fluid interface
4368
         * @see shuffled() - Shuffles the elements in a copy of the map
4369
         */
4370
        public function shuffle( bool $assoc = false ) : self
4371
        {
4372
                if( $assoc )
24✔
4373
                {
4374
                        $list = $this->list();
8✔
4375
                        $keys = array_keys( $list );
8✔
4376
                        shuffle( $keys );
8✔
4377
                        $items = [];
8✔
4378

4379
                        foreach( $keys as $key ) {
8✔
4380
                                $items[$key] = $list[$key];
8✔
4381
                        }
4382

4383
                        $this->list = $items;
8✔
4384
                }
4385
                else
4386
                {
4387
                        shuffle( $this->list() );
16✔
4388
                }
4389

4390
                return $this;
24✔
4391
        }
4392

4393

4394
        /**
4395
         * Shuffles the elements in a copy of the map.
4396
         *
4397
         * Examples:
4398
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled();
4399
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled( true );
4400
         *
4401
         * Results:
4402
         * The map in the first example will contain "a" and "b" in random order and
4403
         * with new keys assigned. The second call will also return all values in
4404
         * random order but preserves the keys of the original list.
4405
         *
4406
         * @param bool $assoc True to preserve keys, false to assign new keys
4407
         * @return self<int|string,mixed> New map with a shuffled copy of the elements
4408
         * @see shuffle() - Shuffles the elements in the map without returning a new map
4409
         */
4410
        public function shuffled( bool $assoc = false ) : self
4411
        {
4412
                return ( clone $this )->shuffle( $assoc );
8✔
4413
        }
4414

4415

4416
        /**
4417
         * Returns a new map with the given number of items skipped.
4418
         *
4419
         * Examples:
4420
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
4421
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
4422
         *      return $item < 4;
4423
         *  } );
4424
         *
4425
         * Results:
4426
         *  [2 => 3, 3 => 4]
4427
         *  [3 => 4]
4428
         *
4429
         * The keys of the items returned in the new map are the same as in the original one.
4430
         *
4431
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4432
         * @return self<int|string,mixed> New map
4433
         */
4434
        public function skip( $offset ) : self
4435
        {
4436
                if( is_numeric( $offset ) ) {
24✔
4437
                        return new static( array_slice( $this->list(), (int) $offset, null, true ) );
8✔
4438
                }
4439

4440
                if( $offset instanceof \Closure ) {
16✔
4441
                        return new static( array_slice( $this->list(), $this->until( $this->list(), $offset ), null, true ) );
8✔
4442
                }
4443

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

4447

4448
        /**
4449
         * Returns a map with the slice from the original map.
4450
         *
4451
         * Examples:
4452
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
4453
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
4454
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
4455
         *
4456
         * Results:
4457
         * The first example will return ['b', 'c'] and the second one ['b'] only.
4458
         * The third example returns ['c'] because the slice starts at the second
4459
         * last value and ends before the last value.
4460
         *
4461
         * The rules for offsets are:
4462
         * - If offset is non-negative, the sequence will start at that offset
4463
         * - If offset is negative, the sequence will start that far from the end
4464
         *
4465
         * Similar for the length:
4466
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4467
         * - If the array is shorter than the length, then only the available array elements will be present
4468
         * - If length is given and is negative then the sequence will stop that many elements from the end
4469
         * - If it is omitted, then the sequence will have everything from offset up until the end
4470
         *
4471
         * The keys of the items returned in the new map are the same as in the original one.
4472
         *
4473
         * @param int $offset Number of elements to start from
4474
         * @param int|null $length Number of elements to return or NULL for no limit
4475
         * @return self<int|string,mixed> New map
4476
         */
4477
        public function slice( int $offset, ?int $length = null ) : self
4478
        {
4479
                return new static( array_slice( $this->list(), $offset, $length, true ) );
48✔
4480
        }
4481

4482

4483
        /**
4484
         * Tests if at least one element passes the test or is part of the map.
4485
         *
4486
         * Examples:
4487
         *  Map::from( ['a', 'b'] )->some( 'a' );
4488
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
4489
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
4490
         *    return $item === 'a';
4491
         *  } );
4492
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
4493
         *  Map::from( ['1', '2'] )->some( [2], true );
4494
         *
4495
         * Results:
4496
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
4497
         *
4498
         * @param \Closure|iterable|mixed $values Closure with (item, key) parameter, element or list of elements to test against
4499
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
4500
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
4501
         */
4502
        public function some( $values, bool $strict = false ) : bool
4503
        {
4504
                $list = $this->list();
48✔
4505

4506
                if( is_iterable( $values ) )
48✔
4507
                {
4508
                        foreach( $values as $entry )
24✔
4509
                        {
4510
                                if( in_array( $entry, $list, $strict ) === true ) {
24✔
4511
                                        return true;
24✔
4512
                                }
4513
                        }
4514

4515
                        return false;
16✔
4516
                }
4517

4518
                if( $values instanceof \Closure )
32✔
4519
                {
4520
                        foreach( $list as $key => $item )
16✔
4521
                        {
4522
                                if( $values( $item, $key ) ) {
16✔
4523
                                        return true;
16✔
4524
                                }
4525
                        }
4526
                }
4527

4528
                return in_array( $values, $list, $strict );
32✔
4529
        }
4530

4531

4532
        /**
4533
         * Sorts all elements in-place using new keys.
4534
         *
4535
         * Examples:
4536
         *  Map::from( ['a' => 1, 'b' => 0] )->sort();
4537
         *  Map::from( [0 => 'b', 1 => 'a'] )->sort();
4538
         *
4539
         * Results:
4540
         *  [0 => 0, 1 => 1]
4541
         *  [0 => 'a', 1 => 'b']
4542
         *
4543
         * The parameter modifies how the values are compared. Possible parameter values are:
4544
         * - SORT_REGULAR : compare elements normally (don't change types)
4545
         * - SORT_NUMERIC : compare elements numerically
4546
         * - SORT_STRING : compare elements as strings
4547
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4548
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4549
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4550
         *
4551
         * The keys aren't preserved and elements get a new index. No new map is created.
4552
         *
4553
         * @param int $options Sort options for PHP sort()
4554
         * @return self<int|string,mixed> Updated map for fluid interface
4555
         * @see sorted() - Sorts elements in a copy of the map
4556
         */
4557
        public function sort( int $options = SORT_REGULAR ) : self
4558
        {
4559
                sort( $this->list(), $options );
40✔
4560
                return $this;
40✔
4561
        }
4562

4563

4564
        /**
4565
         * Sorts the elements in a copy of the map using new keys.
4566
         *
4567
         * Examples:
4568
         *  Map::from( ['a' => 1, 'b' => 0] )->sorted();
4569
         *  Map::from( [0 => 'b', 1 => 'a'] )->sorted();
4570
         *
4571
         * Results:
4572
         *  [0 => 0, 1 => 1]
4573
         *  [0 => 'a', 1 => 'b']
4574
         *
4575
         * The parameter modifies how the values are compared. Possible parameter values are:
4576
         * - SORT_REGULAR : compare elements normally (don't change types)
4577
         * - SORT_NUMERIC : compare elements numerically
4578
         * - SORT_STRING : compare elements as strings
4579
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4580
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4581
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4582
         *
4583
         * The keys aren't preserved and elements get a new index and a new map is created before sorting the elements.
4584
         * Thus, sort() should be preferred for performance reasons if possible. A new map is created by calling this method.
4585
         *
4586
         * @param int $options Sort options for PHP sort()
4587
         * @return self<int|string,mixed> New map with a sorted copy of the elements
4588
         * @see sort() - Sorts elements in-place in the original map
4589
         */
4590
        public function sorted( int $options = SORT_REGULAR ) : self
4591
        {
4592
                return ( clone $this )->sort( $options );
16✔
4593
        }
4594

4595

4596
        /**
4597
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4598
         *
4599
         * Examples:
4600
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4601
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4602
         *
4603
         * Results:
4604
         * The first example removes all entries after "a", so only ['a'] will be left
4605
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4606
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4607
         * ['a', 'x', 'y', 'c'] afterwards.
4608
         *
4609
         * The rules for offsets are:
4610
         * - If offset is non-negative, the sequence will start at that offset
4611
         * - If offset is negative, the sequence will start that far from the end
4612
         *
4613
         * Similar for the length:
4614
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4615
         * - If the array is shorter than the length, then only the available array elements will be present
4616
         * - If length is given and is negative then the sequence will stop that many elements from the end
4617
         * - If it is omitted, then the sequence will have everything from offset up until the end
4618
         *
4619
         * Numerical array indexes are NOT preserved.
4620
         *
4621
         * @param int $offset Number of elements to start from
4622
         * @param int|null $length Number of elements to remove, NULL for all
4623
         * @param mixed $replacement List of elements to insert
4624
         * @return self<int|string,mixed> New map
4625
         */
4626
        public function splice( int $offset, ?int $length = null, $replacement = [] ) : self
4627
        {
4628
                // PHP 7.x doesn't allow to pass NULL as replacement
4629
                if( $length === null ) {
40✔
4630
                        $length = count( $this->list() );
16✔
4631
                }
4632

4633
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
40✔
4634
        }
4635

4636

4637
        /**
4638
         * Returns the strings after the passed value.
4639
         *
4640
         * Examples:
4641
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4642
         *  Map::from( ['abc'] )->strAfter( '' );
4643
         *  Map::from( ['abc'] )->strAfter( 'b' );
4644
         *  Map::from( ['abc'] )->strAfter( 'c' );
4645
         *  Map::from( ['abc'] )->strAfter( 'x' );
4646
         *  Map::from( [''] )->strAfter( '' );
4647
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4648
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4649
         *
4650
         * Results:
4651
         *  ['üß']
4652
         *  ['abc']
4653
         *  ['c']
4654
         *  ['']
4655
         *  []
4656
         *  []
4657
         *  ['1', '1', '1']
4658
         *  ['0', '0']
4659
         *
4660
         * All scalar values (bool, int, float, string) will be converted to strings.
4661
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4662
         *
4663
         * @param string $value Character or string to search for
4664
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4665
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4666
         * @return self<int|string,mixed> New map
4667
         */
4668
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4669
        {
4670
                $list = [];
8✔
4671
                $len = mb_strlen( $value );
8✔
4672
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
8✔
4673

4674
                foreach( $this->list() as $key => $entry )
8✔
4675
                {
4676
                        if( is_scalar( $entry ) )
8✔
4677
                        {
4678
                                $pos = null;
8✔
4679
                                $str = (string) $entry;
8✔
4680

4681
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4682
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
8✔
4683
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4684
                                        $list[$key] = $str;
8✔
4685
                                }
4686
                        }
4687
                }
4688

4689
                return new static( $list );
8✔
4690
        }
4691

4692

4693
        /**
4694
         * Returns the strings before the passed value.
4695
         *
4696
         * Examples:
4697
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4698
         *  Map::from( ['abc'] )->strBefore( '' );
4699
         *  Map::from( ['abc'] )->strBefore( 'b' );
4700
         *  Map::from( ['abc'] )->strBefore( 'a' );
4701
         *  Map::from( ['abc'] )->strBefore( 'x' );
4702
         *  Map::from( [''] )->strBefore( '' );
4703
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4704
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4705
         *
4706
         * Results:
4707
         *  ['äö']
4708
         *  ['abc']
4709
         *  ['a']
4710
         *  ['']
4711
         *  []
4712
         *  []
4713
         *  ['1', '1', '1']
4714
         *  ['0', '0']
4715
         *
4716
         * All scalar values (bool, int, float, string) will be converted to strings.
4717
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4718
         *
4719
         * @param string $value Character or string to search for
4720
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4721
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4722
         * @return self<int|string,mixed> New map
4723
         */
4724
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4725
        {
4726
                $list = [];
8✔
4727
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
8✔
4728

4729
                foreach( $this->list() as $key => $entry )
8✔
4730
                {
4731
                        if( is_scalar( $entry ) )
8✔
4732
                        {
4733
                                $pos = null;
8✔
4734
                                $str = (string) $entry;
8✔
4735

4736
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4737
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
8✔
4738
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4739
                                        $list[$key] = $str;
8✔
4740
                                }
4741
                        }
4742
                }
4743

4744
                return new static( $list );
8✔
4745
        }
4746

4747

4748
        /**
4749
         * Compares the value against all map elements.
4750
         *
4751
         * Examples:
4752
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
4753
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
4754
         *  Map::from( [123, 12.3] )->compare( '12.3' );
4755
         *  Map::from( [false, true] )->compare( '1' );
4756
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
4757
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
4758
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
4759
         *
4760
         * Results:
4761
         * The first four examples return TRUE, the last three examples will return FALSE.
4762
         *
4763
         * All scalar values (bool, float, int and string) are casted to string values before
4764
         * comparing to the given value. Non-scalar values in the map are ignored.
4765
         *
4766
         * @param string $value Value to compare map elements to
4767
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
4768
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
4769
         */
4770
        public function strCompare( string $value, bool $case = true ) : bool
4771
        {
4772
                $fcn = $case ? 'strcmp' : 'strcasecmp';
16✔
4773

4774
                foreach( $this->list() as $item )
16✔
4775
                {
4776
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
16✔
4777
                                return true;
16✔
4778
                        }
4779
                }
4780

4781
                return false;
16✔
4782
        }
4783

4784

4785
        /**
4786
         * Tests if at least one of the passed strings is part of at least one entry.
4787
         *
4788
         * Examples:
4789
         *  Map::from( ['abc'] )->strContains( '' );
4790
         *  Map::from( ['abc'] )->strContains( 'a' );
4791
         *  Map::from( ['abc'] )->strContains( 'bc' );
4792
         *  Map::from( [12345] )->strContains( '23' );
4793
         *  Map::from( [123.4] )->strContains( 23.4 );
4794
         *  Map::from( [12345] )->strContains( false );
4795
         *  Map::from( [12345] )->strContains( true );
4796
         *  Map::from( [false] )->strContains( false );
4797
         *  Map::from( [''] )->strContains( false );
4798
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4799
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4800
         *
4801
         *  Map::from( ['abc'] )->strContains( 'd' );
4802
         *  Map::from( ['abc'] )->strContains( 'cb' );
4803
         *  Map::from( [23456] )->strContains( true );
4804
         *  Map::from( [false] )->strContains( 0 );
4805
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4806
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4807
         *
4808
         * Results:
4809
         * The first eleven examples will return TRUE while the last six will return FALSE.
4810
         *
4811
         * @param array|string $value The string or list of strings to search for in each entry
4812
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4813
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4814
         * @todo 4.0 Add $case parameter at second position
4815
         */
4816
        public function strContains( $value, string $encoding = 'UTF-8' ) : bool
4817
        {
4818
                foreach( $this->list() as $entry )
8✔
4819
                {
4820
                        $entry = (string) $entry;
8✔
4821

4822
                        foreach( (array) $value as $str )
8✔
4823
                        {
4824
                                $str = (string) $str;
8✔
4825

4826
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4827
                                        return true;
8✔
4828
                                }
4829
                        }
4830
                }
4831

4832
                return false;
8✔
4833
        }
4834

4835

4836
        /**
4837
         * Tests if all of the entries contains one of the passed strings.
4838
         *
4839
         * Examples:
4840
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
4841
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
4842
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
4843
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
4844
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
4845
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
4846
         *  Map::from( ['', false] )->strContainsAll( false );
4847
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
4848
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
4849
         *
4850
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
4851
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
4852
         *  Map::from( [23456, '123'] )->strContainsAll( true );
4853
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
4854
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
4855
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
4856
         *
4857
         * Results:
4858
         * The first nine examples will return TRUE while the last six will return FALSE.
4859
         *
4860
         * @param array|string $value The string or list of strings to search for in each entry
4861
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4862
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
4863
         * @todo 4.0 Add $case parameter at second position
4864
         */
4865
        public function strContainsAll( $value, string $encoding = 'UTF-8' ) : bool
4866
        {
4867
                $list = [];
8✔
4868

4869
                foreach( $this->list() as $entry )
8✔
4870
                {
4871
                        $entry = (string) $entry;
8✔
4872
                        $list[$entry] = 0;
8✔
4873

4874
                        foreach( (array) $value as $str )
8✔
4875
                        {
4876
                                $str = (string) $str;
8✔
4877

4878
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4879
                                        $list[$entry] = 1; break;
8✔
4880
                                }
4881
                        }
4882
                }
4883

4884
                return array_sum( $list ) === count( $list );
8✔
4885
        }
4886

4887

4888
        /**
4889
         * Tests if at least one of the entries ends with one of the passed strings.
4890
         *
4891
         * Examples:
4892
         *  Map::from( ['abc'] )->strEnds( '' );
4893
         *  Map::from( ['abc'] )->strEnds( 'c' );
4894
         *  Map::from( ['abc'] )->strEnds( 'bc' );
4895
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
4896
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
4897
         *  Map::from( ['abc'] )->strEnds( 'a' );
4898
         *  Map::from( ['abc'] )->strEnds( 'cb' );
4899
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
4900
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
4901
         *
4902
         * Results:
4903
         * The first five examples will return TRUE while the last four will return FALSE.
4904
         *
4905
         * @param array|string $value The string or strings to search for in each entry
4906
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4907
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
4908
         * @todo 4.0 Add $case parameter at second position
4909
         */
4910
        public function strEnds( $value, string $encoding = 'UTF-8' ) : bool
4911
        {
4912
                foreach( $this->list() as $entry )
8✔
4913
                {
4914
                        $entry = (string) $entry;
8✔
4915

4916
                        foreach( (array) $value as $str )
8✔
4917
                        {
4918
                                $len = mb_strlen( (string) $str );
8✔
4919

4920
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4921
                                        return true;
8✔
4922
                                }
4923
                        }
4924
                }
4925

4926
                return false;
8✔
4927
        }
4928

4929

4930
        /**
4931
         * Tests if all of the entries ends with at least one of the passed strings.
4932
         *
4933
         * Examples:
4934
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
4935
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
4936
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
4937
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
4938
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
4939
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
4940
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
4941
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
4942
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
4943
         *
4944
         * Results:
4945
         * The first five examples will return TRUE while the last four will return FALSE.
4946
         *
4947
         * @param array|string $value The string or strings to search for in each entry
4948
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4949
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4950
         * @todo 4.0 Add $case parameter at second position
4951
         */
4952
        public function strEndsAll( $value, string $encoding = 'UTF-8' ) : bool
4953
        {
4954
                $list = [];
8✔
4955

4956
                foreach( $this->list() as $entry )
8✔
4957
                {
4958
                        $entry = (string) $entry;
8✔
4959
                        $list[$entry] = 0;
8✔
4960

4961
                        foreach( (array) $value as $str )
8✔
4962
                        {
4963
                                $len = mb_strlen( (string) $str );
8✔
4964

4965
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4966
                                        $list[$entry] = 1; break;
8✔
4967
                                }
4968
                        }
4969
                }
4970

4971
                return array_sum( $list ) === count( $list );
8✔
4972
        }
4973

4974

4975
        /**
4976
         * Returns an element by key and casts it to string if possible.
4977
         *
4978
         * Examples:
4979
         *  Map::from( ['a' => true] )->string( 'a' );
4980
         *  Map::from( ['a' => 1] )->string( 'a' );
4981
         *  Map::from( ['a' => 1.1] )->string( 'a' );
4982
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
4983
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
4984
         *  Map::from( [] )->string( 'a', function() { return 'no'; } );
4985
         *
4986
         *  Map::from( [] )->string( 'b' );
4987
         *  Map::from( ['b' => ''] )->string( 'b' );
4988
         *  Map::from( ['b' => null] )->string( 'b' );
4989
         *  Map::from( ['b' => [true]] )->string( 'b' );
4990
         *  Map::from( ['b' => resource] )->string( 'b' );
4991
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
4992
         *
4993
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
4994
         *
4995
         * Results:
4996
         * The first six examples will return the value as string while the 9th to 12th
4997
         * example returns an empty string. The last example will throw an exception.
4998
         *
4999
         * This does also work for multi-dimensional arrays by passing the keys
5000
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5001
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5002
         * public properties of objects or objects implementing __isset() and __get() methods.
5003
         *
5004
         * @param int|string $key Key or path to the requested item
5005
         * @param mixed $default Default value if key isn't found (will be casted to bool)
5006
         * @return string Value from map or default value
5007
         */
5008
        public function string( $key, $default = '' ) : string
5009
        {
5010
                return (string) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
5011
        }
5012

5013

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

5040
                return $this;
8✔
5041
        }
5042

5043

5044
        /**
5045
         * Replaces all occurrences of the search string with the replacement string.
5046
         *
5047
         * Examples:
5048
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
5049
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
5050
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
5051
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
5052
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
5053
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
5054
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
5055
         *
5056
         * Restults:
5057
         * ['google.de', 'aimeos.de']
5058
         * ['google.de', 'aimeos.de']
5059
         * ['google.de', 'aimeos']
5060
         * ['google.fr', 'aimeos.de']
5061
         * ['google.de', 'aimeos.de']
5062
         * ['google.de', 'aimeos.de', 123]
5063
         * ['GOOGLE.de', 'AIMEOS.de']
5064
         *
5065
         * If you use an array of strings for search or search/replacement, the order of
5066
         * the strings matters! Each search string found is replaced by the corresponding
5067
         * replacement string at the same position.
5068
         *
5069
         * In case of array parameters and if the number of replacement strings is less
5070
         * than the number of search strings, the search strings with no corresponding
5071
         * replacement string are replaced with empty strings. Replacement strings with
5072
         * no corresponding search string are ignored.
5073
         *
5074
         * An array parameter for the replacements is only allowed if the search parameter
5075
         * is an array of strings too!
5076
         *
5077
         * Because the method replaces from left to right, it might replace a previously
5078
         * inserted value when doing multiple replacements. Entries which are non-string
5079
         * values are left untouched.
5080
         *
5081
         * @param array|string $search String or list of strings to search for
5082
         * @param array|string $replace String or list of strings of replacement strings
5083
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
5084
         * @return self<int|string,mixed> Updated map for fluid interface
5085
         */
5086
        public function strReplace( $search, $replace, bool $case = false ) : self
5087
        {
5088
                $fcn = $case ? 'str_ireplace' : 'str_replace';
8✔
5089

5090
                foreach( $this->list() as &$entry )
8✔
5091
                {
5092
                        if( is_string( $entry ) ) {
8✔
5093
                                $entry = $fcn( $search, $replace, $entry );
8✔
5094
                        }
5095
                }
5096

5097
                return $this;
8✔
5098
        }
5099

5100

5101
        /**
5102
         * Tests if at least one of the entries starts with at least one of the passed strings.
5103
         *
5104
         * Examples:
5105
         *  Map::from( ['abc'] )->strStarts( '' );
5106
         *  Map::from( ['abc'] )->strStarts( 'a' );
5107
         *  Map::from( ['abc'] )->strStarts( 'ab' );
5108
         *  Map::from( ['abc'] )->strStarts( ['a', 'b'] );
5109
         *  Map::from( ['abc'] )->strStarts( 'ab', 'ASCII' );
5110
         *  Map::from( ['abc'] )->strStarts( 'b' );
5111
         *  Map::from( ['abc'] )->strStarts( 'bc' );
5112
         *  Map::from( ['abc'] )->strStarts( ['b', 'c'] );
5113
         *  Map::from( ['abc'] )->strStarts( 'bc', 'ASCII' );
5114
         *
5115
         * Results:
5116
         * The first five examples will return TRUE while the last four will return FALSE.
5117
         *
5118
         * @param array|string $value The string or strings to search for in each entry
5119
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5120
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
5121
         * @todo 4.0 Add $case parameter at second position
5122
         */
5123
        public function strStarts( $value, string $encoding = 'UTF-8' ) : bool
5124
        {
5125
                foreach( $this->list() as $entry )
8✔
5126
                {
5127
                        $entry = (string) $entry;
8✔
5128

5129
                        foreach( (array) $value as $str )
8✔
5130
                        {
5131
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
5132
                                        return true;
8✔
5133
                                }
5134
                        }
5135
                }
5136

5137
                return false;
8✔
5138
        }
5139

5140

5141
        /**
5142
         * Tests if all of the entries starts with one of the passed strings.
5143
         *
5144
         * Examples:
5145
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
5146
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
5147
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
5148
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
5149
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
5150
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
5151
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
5152
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
5153
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
5154
         *
5155
         * Results:
5156
         * The first five examples will return TRUE while the last four will return FALSE.
5157
         *
5158
         * @param array|string $value The string or strings to search for in each entry
5159
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
5160
         * @return bool TRUE if one of the entries starts with one of the strings, FALSE if not
5161
         * @todo 4.0 Add $case parameter at second position
5162
         */
5163
        public function strStartsAll( $value, string $encoding = 'UTF-8' ) : bool
5164
        {
5165
                $list = [];
8✔
5166

5167
                foreach( $this->list() as $entry )
8✔
5168
                {
5169
                        $entry = (string) $entry;
8✔
5170
                        $list[$entry] = 0;
8✔
5171

5172
                        foreach( (array) $value as $str )
8✔
5173
                        {
5174
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
5175
                                        $list[$entry] = 1; break;
8✔
5176
                                }
5177
                        }
5178
                }
5179

5180
                return array_sum( $list ) === count( $list );
8✔
5181
        }
5182

5183

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

5210
                return $this;
8✔
5211
        }
5212

5213

5214
        /**
5215
         * Adds a suffix at the end of each map entry.
5216
         *
5217
         * By defaul, nested arrays are walked recusively so all entries at all levels are suffixed.
5218
         *
5219
         * Examples:
5220
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
5221
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
5222
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
5223
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
5224
         *      return '-' . ( ord( $item ) + ord( $key ) );
5225
         *  } );
5226
         *
5227
         * Results:
5228
         *  The first example returns ['a-1', 'b-1'] while the second one will return
5229
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
5230
         *  level only so it will return ['a-1', ['b']]. The forth example passing
5231
         *  the closure will return ['a-145', 'b-147'].
5232
         *
5233
         * The keys are preserved using this method.
5234
         *
5235
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
5236
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
5237
         * @return self<int|string,mixed> Updated map for fluid interface
5238
         */
5239
        public function suffix( $suffix, ?int $depth = null ) : self
5240
        {
5241
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
6✔
5242

5243
                        foreach( $list as $key => $item )
8✔
5244
                        {
5245
                                if( is_array( $item ) ) {
8✔
5246
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
8✔
5247
                                } else {
5248
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
8✔
5249
                                }
5250
                        }
5251

5252
                        return $list;
8✔
5253
                };
8✔
5254

5255
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
8✔
5256
                return $this;
8✔
5257
        }
5258

5259

5260
        /**
5261
         * Returns the sum of all integer and float values in the map.
5262
         *
5263
         * Examples:
5264
         *  Map::from( [1, 3, 5] )->sum();
5265
         *  Map::from( [1, 'sum', 5] )->sum();
5266
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
5267
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
5268
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( fn( $val, $key ) => $val['i']['p'] ?? null )
5269
         *  Map::from( [30, 50, 10] )->sum( fn( $val, $key ) => $val < 50 ? $val : null )
5270
         *
5271
         * Results:
5272
         * The first line will return "9", the second one "6", the third one "90"
5273
         * the forth/fifth "80" and the last one "40".
5274
         *
5275
         * Non-numeric values will be removed before calculation.
5276
         *
5277
         * This does also work for multi-dimensional arrays by passing the keys
5278
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5279
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5280
         * public properties of objects or objects implementing __isset() and __get() methods.
5281
         *
5282
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to sum up
5283
         * @return float Sum of all elements or 0 if there are no elements in the map
5284
         */
5285
        public function sum( $col = null ) : float
5286
        {
5287
                $list = $this->list();
24✔
5288
                $vals = array_filter( $col ? array_map( $this->mapper( $col ), $list, array_keys( $list ) ) : $list, 'is_numeric' );
24✔
5289

5290
                return array_sum( $vals );
24✔
5291
        }
5292

5293

5294
        /**
5295
         * Returns a new map with the given number of items.
5296
         *
5297
         * The keys of the items returned in the new map are the same as in the original one.
5298
         *
5299
         * Examples:
5300
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
5301
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
5302
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
5303
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
5304
         *      return $item < 2;
5305
         *  } );
5306
         *
5307
         * Results:
5308
         *  [0 => 1, 1 => 2]
5309
         *  [1 => 2, 2 => 3]
5310
         *  [2 => 3, 3 => 4]
5311
         *  [1 => 2, 2 => 3]
5312
         *
5313
         * The keys of the items returned in the new map are the same as in the original one.
5314
         *
5315
         * @param int $size Number of items to return
5316
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5317
         * @return self<int|string,mixed> New map
5318
         */
5319
        public function take( int $size, $offset = 0 ) : self
5320
        {
5321
                $list = $this->list();
40✔
5322

5323
                if( is_numeric( $offset ) ) {
40✔
5324
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
24✔
5325
                }
5326

5327
                if( $offset instanceof \Closure ) {
16✔
5328
                        return new static( array_slice( $list, $this->until( $list, $offset ), $size, true ) );
8✔
5329
                }
5330

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

5334

5335
        /**
5336
         * Passes a clone of the map to the given callback.
5337
         *
5338
         * Use it to "tap" into a chain of methods to check the state between two
5339
         * method calls. The original map is not altered by anything done in the
5340
         * callback.
5341
         *
5342
         * Examples:
5343
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
5344
         *    print_r( $map->remove( 0 )->toArray() );
5345
         *  } )->first();
5346
         *
5347
         * Results:
5348
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
5349
         * then prints the items without the first (`[2, 3]`) in the function passed
5350
         * to `tap()` and returns the first item ("1") at the end.
5351
         *
5352
         * @param callable $callback Function receiving ($map) parameter
5353
         * @return self<int|string,mixed> Same map for fluid interface
5354
         */
5355
        public function tap( callable $callback ) : self
5356
        {
5357
                $callback( clone $this );
8✔
5358
                return $this;
8✔
5359
        }
5360

5361

5362
        /**
5363
         * Returns the elements as a plain array.
5364
         *
5365
         * @return array<int|string,mixed> Plain array
5366
         */
5367
        public function to() : array
5368
        {
5369
                return $this->list = $this->array( $this->list );
8✔
5370
        }
5371

5372

5373
        /**
5374
         * Returns the elements as a plain array.
5375
         *
5376
         * @return array<int|string,mixed> Plain array
5377
         */
5378
        public function toArray() : array
5379
        {
5380
                return $this->list = $this->array( $this->list );
1,936✔
5381
        }
5382

5383

5384
        /**
5385
         * Returns the elements encoded as JSON string.
5386
         *
5387
         * There are several options available to modify the JSON output:
5388
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5389
         * The parameter can be a single JSON_* constant or a bitmask of several
5390
         * constants combine by bitwise OR (|), e.g.:
5391
         *
5392
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5393
         *
5394
         * @param int $options Combination of JSON_* constants
5395
         * @return string|null Array encoded as JSON string or NULL on failure
5396
         */
5397
        public function toJson( int $options = 0 ) : ?string
5398
        {
5399
                $result = json_encode( $this->list(), $options );
16✔
5400
                return $result !== false ? $result : null;
16✔
5401
        }
5402

5403

5404
        /**
5405
         * Reverses the element order in a copy of the map (alias).
5406
         *
5407
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5408
         * preferred because it uses one method call less than toReversed().
5409
         *
5410
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5411
         * @see reversed() - Underlying method with same parameters and return value but better performance
5412
         */
5413
        public function toReversed() : self
5414
        {
5415
                return $this->reversed();
8✔
5416
        }
5417

5418

5419
        /**
5420
         * Sorts the elements in a copy of the map using new keys (alias).
5421
         *
5422
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5423
         * preferred because it uses one method call less than toSorted().
5424
         *
5425
         * @param int $options Sort options for PHP sort()
5426
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5427
         * @see sorted() - Underlying method with same parameters and return value but better performance
5428
         */
5429
        public function toSorted( int $options = SORT_REGULAR ) : self
5430
        {
5431
                return $this->sorted( $options );
8✔
5432
        }
5433

5434

5435
        /**
5436
         * Creates a HTTP query string from the map elements.
5437
         *
5438
         * Examples:
5439
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5440
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5441
         *
5442
         * Results:
5443
         *  a=1&b=2
5444
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5445
         *
5446
         * @return string Parameter string for GET requests
5447
         */
5448
        public function toUrl() : string
5449
        {
5450
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
16✔
5451
        }
5452

5453

5454
        /**
5455
         * Creates new key/value pairs using the passed function and returns a new map for the result.
5456
         *
5457
         * Examples:
5458
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5459
         *      return [$key . '-2' => $value * 2];
5460
         *  } );
5461
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5462
         *      return [$key => $value * 2, $key . $key => $value * 4];
5463
         *  } );
5464
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5465
         *      return $key < 'b' ? [$key => $value * 2] : null;
5466
         *  } );
5467
         *  Map::from( ['la' => 2, 'le' => 4, 'li' => 6] )->transform( function( $value, $key ) {
5468
         *      return [$key[0] => $value * 2];
5469
         *  } );
5470
         *
5471
         * Results:
5472
         *  ['a-2' => 4, 'b-2' => 8]
5473
         *  ['a' => 4, 'aa' => 8, 'b' => 8, 'bb' => 16]
5474
         *  ['a' => 4]
5475
         *  ['l' => 12]
5476
         *
5477
         * If a key is returned twice, the last value will overwrite previous values.
5478
         *
5479
         * @param \Closure $callback Function with (value, key) parameters and returns an array of new key/value pair(s)
5480
         * @return self<int|string,mixed> New map with the new key/value pairs
5481
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
5482
         * @see rekey() - Changes the keys according to the passed function
5483
         */
5484
        public function transform( \Closure $callback ) : self
5485
        {
5486
                $result = [];
32✔
5487

5488
                foreach( $this->list() as $key => $value )
32✔
5489
                {
5490
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
32✔
5491
                                $result[$newkey] = $newval;
32✔
5492
                        }
5493
                }
5494

5495
                return new static( $result );
32✔
5496
        }
5497

5498

5499
        /**
5500
         * Exchanges rows and columns for a two dimensional map.
5501
         *
5502
         * Examples:
5503
         *  Map::from( [
5504
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5505
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5506
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5507
         *  ] )->transpose();
5508
         *
5509
         *  Map::from( [
5510
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5511
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5512
         *    ['name' => 'C', 2020 => 400]
5513
         *  ] );
5514
         *
5515
         * Results:
5516
         *  [
5517
         *    'name' => ['A', 'B', 'C'],
5518
         *    2020 => [200, 300, 400],
5519
         *    2021 => [100, 200, 300],
5520
         *    2022 => [50, 100, 200]
5521
         *  ]
5522
         *
5523
         *  [
5524
         *    'name' => ['A', 'B', 'C'],
5525
         *    2020 => [200, 300, 400],
5526
         *    2021 => [100, 200],
5527
         *    2022 => [50]
5528
         *  ]
5529
         *
5530
         * @return self<int|string,mixed> New map
5531
         */
5532
        public function transpose() : self
5533
        {
5534
                $result = [];
16✔
5535

5536
                foreach( (array) $this->first( [] ) as $key => $col ) {
16✔
5537
                        $result[$key] = array_column( $this->list(), $key );
16✔
5538
                }
5539

5540
                return new static( $result );
16✔
5541
        }
5542

5543

5544
        /**
5545
         * Traverses trees of nested items passing each item to the callback.
5546
         *
5547
         * This does work for nested arrays and objects with public properties or
5548
         * objects implementing __isset() and __get() methods. To build trees
5549
         * of nested items, use the tree() method.
5550
         *
5551
         * Examples:
5552
         *   Map::from( [[
5553
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5554
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5555
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5556
         *     ]
5557
         *   ]] )->traverse();
5558
         *
5559
         *   Map::from( [[
5560
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5561
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5562
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5563
         *     ]
5564
         *   ]] )->traverse( function( $entry, $key, $level, $parent ) {
5565
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
5566
         *   } );
5567
         *
5568
         *   Map::from( [[
5569
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5570
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5571
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5572
         *     ]
5573
         *   ]] )->traverse( function( &$entry, $key, $level, $parent ) {
5574
         *     $entry['path'] = isset( $parent['path'] ) ? $parent['path'] . '/' . $entry['name'] : $entry['name'];
5575
         *     return $entry;
5576
         *   } );
5577
         *
5578
         *   Map::from( [[
5579
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
5580
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
5581
         *     ]
5582
         *   ]] )->traverse( null, 'nodes' );
5583
         *
5584
         * Results:
5585
         *   [
5586
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
5587
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5588
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
5589
         *   ]
5590
         *
5591
         *   ['- n1', '-- n2', '-- n3']
5592
         *
5593
         *   [
5594
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...], 'path' => 'n1'],
5595
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => [], 'path' => 'n1/n2'],
5596
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => [], 'path' => 'n1/n3'],
5597
         *   ]
5598
         *
5599
         *   [
5600
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
5601
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
5602
         *   ]
5603
         *
5604
         * @param \Closure|null $callback Callback with (entry, key, level, $parent) arguments, returns the entry added to result
5605
         * @param string $nestKey Key to the children of each item
5606
         * @return self<int|string,mixed> New map with all items as flat list
5607
         */
5608
        public function traverse( ?\Closure $callback = null, string $nestKey = 'children' ) : self
5609
        {
5610
                $result = [];
40✔
5611
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
40✔
5612

5613
                return map( $result );
40✔
5614
        }
5615

5616

5617
        /**
5618
         * Creates a tree structure from the list items.
5619
         *
5620
         * Use this method to rebuild trees e.g. from database records. To traverse
5621
         * trees, use the traverse() method.
5622
         *
5623
         * Examples:
5624
         *  Map::from( [
5625
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
5626
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
5627
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
5628
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
5629
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
5630
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
5631
         *  ] )->tree( 'id', 'pid' );
5632
         *
5633
         * Results:
5634
         *   [1 => [
5635
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
5636
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
5637
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
5638
         *       ]],
5639
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
5640
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
5641
         *       ]],
5642
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
5643
         *     ]
5644
         *   ]]
5645
         *
5646
         * To build the tree correctly, the items must be in order or at least the
5647
         * nodes of the lower levels must come first. For a tree like this:
5648
         * n1
5649
         * |- n2
5650
         * |  |- n3
5651
         * |- n4
5652
         * |  |- n5
5653
         * |- n6
5654
         *
5655
         * Accepted item order:
5656
         * - in order: n1, n2, n3, n4, n5, n6
5657
         * - lower levels first: n1, n2, n4, n6, n3, n5
5658
         *
5659
         * If your items are unordered, apply usort() first to the map entries, e.g.
5660
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
5661
         *     return $item1['lvl'] <=> $item2['lvl'];
5662
         *   } );
5663
         *
5664
         * @param string $idKey Name of the key with the unique ID of the node
5665
         * @param string $parentKey Name of the key with the ID of the parent node
5666
         * @param string $nestKey Name of the key with will contain the children of the node
5667
         * @return self<int|string,mixed> New map with one or more root tree nodes
5668
         */
5669
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
5670
        {
5671
                $this->list();
8✔
5672
                $trees = $refs = [];
8✔
5673

5674
                foreach( $this->list as &$node )
8✔
5675
                {
5676
                        $node[$nestKey] = [];
8✔
5677
                        $refs[$node[$idKey]] = &$node;
8✔
5678

5679
                        if( $node[$parentKey] ) {
8✔
5680
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
8✔
5681
                        } else {
5682
                                $trees[$node[$idKey]] = &$node;
8✔
5683
                        }
5684
                }
5685

5686
                return map( $trees );
8✔
5687
        }
5688

5689

5690
        /**
5691
         * Removes the passed characters from the left/right of all strings.
5692
         *
5693
         * Examples:
5694
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5695
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5696
         *
5697
         * Results:
5698
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5699
         *
5700
         * @param string $chars List of characters to trim
5701
         * @return self<int|string,mixed> Updated map for fluid interface
5702
         */
5703
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5704
        {
5705
                foreach( $this->list() as &$entry )
8✔
5706
                {
5707
                        if( is_string( $entry ) ) {
8✔
5708
                                $entry = trim( $entry, $chars );
8✔
5709
                        }
5710
                }
5711

5712
                return $this;
8✔
5713
        }
5714

5715

5716
        /**
5717
         * Sorts all elements using a callback and maintains the key association.
5718
         *
5719
         * The given callback will be used to compare the values. The callback must accept
5720
         * two parameters (item A and B) and must return -1 if item A is smaller than
5721
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5722
         * method name and an anonymous function can be passed.
5723
         *
5724
         * Examples:
5725
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( 'strcasecmp' );
5726
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( function( $itemA, $itemB ) {
5727
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5728
         *  } );
5729
         *
5730
         * Results:
5731
         *  ['b' => 'a', 'a' => 'B']
5732
         *  ['b' => 'a', 'a' => 'B']
5733
         *
5734
         * The keys are preserved using this method and no new map is created.
5735
         *
5736
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5737
         * @return self<int|string,mixed> Updated map for fluid interface
5738
         */
5739
        public function uasort( callable $callback ) : self
5740
        {
5741
                uasort( $this->list(), $callback );
16✔
5742
                return $this;
16✔
5743
        }
5744

5745

5746
        /**
5747
         * Sorts all elements using a callback and maintains the key association.
5748
         *
5749
         * The given callback will be used to compare the values. The callback must accept
5750
         * two parameters (item A and B) and must return -1 if item A is smaller than
5751
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5752
         * method name and an anonymous function can be passed.
5753
         *
5754
         * Examples:
5755
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( 'strcasecmp' );
5756
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( function( $itemA, $itemB ) {
5757
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5758
         *  } );
5759
         *
5760
         * Results:
5761
         *  ['b' => 'a', 'a' => 'B']
5762
         *  ['b' => 'a', 'a' => 'B']
5763
         *
5764
         * The keys are preserved using this method and a new map is created.
5765
         *
5766
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5767
         * @return self<int|string,mixed> Updated map for fluid interface
5768
         */
5769
        public function uasorted( callable $callback ) : self
5770
        {
5771
                return ( clone $this )->uasort( $callback );
8✔
5772
        }
5773

5774

5775
        /**
5776
         * Sorts the map elements by their keys using a callback.
5777
         *
5778
         * The given callback will be used to compare the keys. The callback must accept
5779
         * two parameters (key A and B) and must return -1 if key A is smaller than
5780
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5781
         * method name and an anonymous function can be passed.
5782
         *
5783
         * Examples:
5784
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( 'strcasecmp' );
5785
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( function( $keyA, $keyB ) {
5786
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5787
         *  } );
5788
         *
5789
         * Results:
5790
         *  ['a' => 'b', 'B' => 'a']
5791
         *  ['a' => 'b', 'B' => 'a']
5792
         *
5793
         * The keys are preserved using this method and no new map is created.
5794
         *
5795
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5796
         * @return self<int|string,mixed> Updated map for fluid interface
5797
         */
5798
        public function uksort( callable $callback ) : self
5799
        {
5800
                uksort( $this->list(), $callback );
16✔
5801
                return $this;
16✔
5802
        }
5803

5804

5805
        /**
5806
         * Sorts a copy of the map elements by their keys using a callback.
5807
         *
5808
         * The given callback will be used to compare the keys. The callback must accept
5809
         * two parameters (key A and B) and must return -1 if key A is smaller than
5810
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5811
         * method name and an anonymous function can be passed.
5812
         *
5813
         * Examples:
5814
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5815
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5816
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5817
         *  } );
5818
         *
5819
         * Results:
5820
         *  ['a' => 'b', 'B' => 'a']
5821
         *  ['a' => 'b', 'B' => 'a']
5822
         *
5823
         * The keys are preserved using this method and a new map is created.
5824
         *
5825
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5826
         * @return self<int|string,mixed> Updated map for fluid interface
5827
         */
5828
        public function uksorted( callable $callback ) : self
5829
        {
5830
                return ( clone $this )->uksort( $callback );
8✔
5831
        }
5832

5833

5834
        /**
5835
         * Unflattens the key path/value pairs into a multi-dimensional array.
5836
         *
5837
         * Examples:
5838
         *  Map::from( ['a/b/c' => 1, 'a/b/d' => 2, 'b/e' => 3] )->unflatten();
5839
         *  Map::from( ['a.b.c' => 1, 'a.b.d' => 2, 'b.e' => 3] )->sep( '.' )->unflatten();
5840
         *
5841
         * Results:
5842
         * ['a' => ['b' => ['c' => 1, 'd' => 2]], 'b' => ['e' => 3]]
5843
         *
5844
         * This is the inverse method for flatten().
5845
         *
5846
         * @return self<int|string,mixed> New map with multi-dimensional arrays
5847
         */
5848
        public function unflatten() : self
5849
        {
5850
                $result = [];
8✔
5851

5852
                foreach( $this->list() as $key => $value )
8✔
5853
                {
5854
                        $nested = &$result;
8✔
5855
                        $parts = explode( $this->sep, $key );
8✔
5856

5857
                        while( count( $parts ) > 1 ) {
8✔
5858
                                $nested = &$nested[array_shift( $parts )] ?? [];
8✔
5859
                        }
5860

5861
                        $nested[array_shift( $parts )] = $value;
8✔
5862
                }
5863

5864
                return new static( $result );
8✔
5865
        }
5866

5867

5868
        /**
5869
         * Builds a union of the elements and the given elements without overwriting existing ones.
5870
         * Existing keys in the map will not be overwritten
5871
         *
5872
         * Examples:
5873
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
5874
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
5875
         *
5876
         * Results:
5877
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
5878
         * isn't overwritten. In the second example, the result will be a combined
5879
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
5880
         *
5881
         * If list entries should be overwritten,  please use merge() instead!
5882
         * The keys are preserved using this method and no new map is created.
5883
         *
5884
         * @param iterable<int|string,mixed> $elements List of elements
5885
         * @return self<int|string,mixed> Updated map for fluid interface
5886
         */
5887
        public function union( iterable $elements ) : self
5888
        {
5889
                $this->list = $this->list() + $this->array( $elements );
16✔
5890
                return $this;
16✔
5891
        }
5892

5893

5894
        /**
5895
         * Returns only unique elements from the map incl. their keys.
5896
         *
5897
         * Examples:
5898
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
5899
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
5900
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
5901
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( fn( $item, $key ) => $item['i']['p'] )
5902
         *
5903
         * Results:
5904
         * [0 => 'a', 1 => 'b', 3 => 'c']
5905
         * [['p' => 1], ['p' => 2]]
5906
         * [['i' => ['p' => '1']]]
5907
         * [['i' => ['p' => '1']]]
5908
         *
5909
         * Two elements are considered equal if comparing their string representions returns TRUE:
5910
         * (string) $elem1 === (string) $elem2
5911
         *
5912
         * The keys of the elements are only preserved in the new map if no key is passed.
5913
         *
5914
         * @param \Closure|string|null $col Key, path of the nested array or anonymous function with ($item, $key) parameters returning the value for comparison
5915
         * @return self<int|string,mixed> New map
5916
         */
5917
        public function unique( $col = null ) : self
5918
        {
5919
                if( $col === null ) {
40✔
5920
                        return new static( array_unique( $this->list() ) );
16✔
5921
                }
5922

5923
                $list = $this->list();
24✔
5924
                $map = array_map( $this->mapper( $col ), array_values( $list ), array_keys( $list ) );
24✔
5925

5926
                return new static( array_intersect_key( $list, array_unique( $map ) ) );
24✔
5927
        }
5928

5929

5930
        /**
5931
         * Pushes an element onto the beginning of the map without returning a new map.
5932
         *
5933
         * Examples:
5934
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
5935
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
5936
         *
5937
         * Results:
5938
         *  ['d', 'a', 'b']
5939
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
5940
         *
5941
         * The keys of the elements are only preserved in the new map if no key is passed.
5942
         *
5943
         * Performance note:
5944
         * The bigger the list, the higher the performance impact because unshift()
5945
         * needs to create a new list and copies all existing elements to the new
5946
         * array. Usually, it's better to push() new entries at the end and reverse()
5947
         * the list afterwards:
5948
         *
5949
         *  $map->push( 'a' )->push( 'b' )->reverse();
5950
         * instead of
5951
         *  $map->unshift( 'a' )->unshift( 'b' );
5952
         *
5953
         * @param mixed $value Item to add at the beginning
5954
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
5955
         * @return self<int|string,mixed> Updated map for fluid interface
5956
         */
5957
        public function unshift( $value, $key = null ) : self
5958
        {
5959
                if( $key === null ) {
24✔
5960
                        array_unshift( $this->list(), $value );
16✔
5961
                } else {
5962
                        $this->list = [$key => $value] + $this->list();
8✔
5963
                }
5964

5965
                return $this;
24✔
5966
        }
5967

5968

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

5998

5999
        /**
6000
         * Sorts a copy of all elements using a callback using new keys.
6001
         *
6002
         * The given callback will be used to compare the values. The callback must accept
6003
         * two parameters (item A and B) and must return -1 if item A is smaller than
6004
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
6005
         * method name and an anonymous function can be passed.
6006
         *
6007
         * Examples:
6008
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
6009
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
6010
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
6011
         *  } );
6012
         *
6013
         * Results:
6014
         *  [0 => 'a', 1 => 'B']
6015
         *  [0 => 'a', 1 => 'B']
6016
         *
6017
         * The keys aren't preserved, elements get a new index and a new map is created.
6018
         *
6019
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
6020
         * @return self<int|string,mixed> Updated map for fluid interface
6021
         */
6022
        public function usorted( callable $callback ) : self
6023
        {
6024
                return ( clone $this )->usort( $callback );
8✔
6025
        }
6026

6027

6028
        /**
6029
         * Resets the keys and return the values in a new map.
6030
         *
6031
         * Examples:
6032
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
6033
         *
6034
         * Results:
6035
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
6036
         *
6037
         * @return self<int|string,mixed> New map of the values
6038
         */
6039
        public function values() : self
6040
        {
6041
                return new static( array_values( $this->list() ) );
80✔
6042
        }
6043

6044

6045
        /**
6046
         * Applies the given callback to all elements.
6047
         *
6048
         * To change the values of the Map, specify the value parameter as reference
6049
         * (&$value). You can only change the values but not the keys nor the array
6050
         * structure.
6051
         *
6052
         * Examples:
6053
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
6054
         *    $value = strtoupper( $value );
6055
         *  } );
6056
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
6057
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
6058
         *  } );
6059
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
6060
         *    $value = $data[$value] ?? $value;
6061
         *  }, [1 => 'one', 2 => 'two'] );
6062
         *
6063
         * Results:
6064
         * The first example will change the Map elements to:
6065
         *   ['A', 'B', ['C', 'D'], 'E']
6066
         * The output of the second one will be:
6067
         *  ASCII 66 is B
6068
         *  ASCII 97 is a
6069
         * The last example changes the Map elements to:
6070
         *  ['one', 'two', 3]
6071
         *
6072
         * By default, Map elements which are arrays will be traversed recursively.
6073
         * To iterate over the Map elements only, pass FALSE as third parameter.
6074
         *
6075
         * @param callable $callback Function with (item, key, data) parameters
6076
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
6077
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
6078
         * @return self<int|string,mixed> Updated map for fluid interface
6079
         */
6080
        public function walk( callable $callback, $data = null, bool $recursive = true ) : self
6081
        {
6082
                if( $recursive ) {
24✔
6083
                        array_walk_recursive( $this->list(), $callback, $data );
16✔
6084
                } else {
6085
                        array_walk( $this->list(), $callback, $data );
8✔
6086
                }
6087

6088
                return $this;
24✔
6089
        }
6090

6091

6092
        /**
6093
         * Filters the list of elements by a given condition.
6094
         *
6095
         * Examples:
6096
         *  Map::from( [
6097
         *    ['id' => 1, 'type' => 'name'],
6098
         *    ['id' => 2, 'type' => 'short'],
6099
         *  ] )->where( 'type', '==', 'name' );
6100
         *
6101
         *  Map::from( [
6102
         *    ['id' => 3, 'price' => 10],
6103
         *    ['id' => 4, 'price' => 50],
6104
         *  ] )->where( 'price', '>', 20 );
6105
         *
6106
         *  Map::from( [
6107
         *    ['id' => 3, 'price' => 10],
6108
         *    ['id' => 4, 'price' => 50],
6109
         *  ] )->where( 'price', 'in', [10, 25] );
6110
         *
6111
         *  Map::from( [
6112
         *    ['id' => 3, 'price' => 10],
6113
         *    ['id' => 4, 'price' => 50],
6114
         *  ] )->where( 'price', '-', [10, 100] );
6115
         *
6116
         *  Map::from( [
6117
         *    ['item' => ['id' => 3, 'price' => 10]],
6118
         *    ['item' => ['id' => 4, 'price' => 50]],
6119
         *  ] )->where( 'item/price', '>', 30 );
6120
         *
6121
         * Results:
6122
         *  [0 => ['id' => 1, 'type' => 'name']]
6123
         *  [1 => ['id' => 4, 'price' => 50]]
6124
         *  [0 => ['id' => 3, 'price' => 10]]
6125
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
6126
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
6127
         *
6128
         * Available operators are:
6129
         * * '==' : Equal
6130
         * * '===' : Equal and same type
6131
         * * '!=' : Not equal
6132
         * * '!==' : Not equal and same type
6133
         * * '<=' : Smaller than an equal
6134
         * * '>=' : Greater than an equal
6135
         * * '<' : Smaller
6136
         * * '>' : Greater
6137
         * 'in' : Array of value which are in the list of values
6138
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
6139
         *
6140
         * This does also work for multi-dimensional arrays by passing the keys
6141
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
6142
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
6143
         * public properties of objects or objects implementing __isset() and __get() methods.
6144
         *
6145
         * The keys of the original map are preserved in the returned map.
6146
         *
6147
         * @param string $key Key or path of the value in the array or object used for comparison
6148
         * @param string $op Operator used for comparison
6149
         * @param mixed $value Value used for comparison
6150
         * @return self<int|string,mixed> New map for fluid interface
6151
         */
6152
        public function where( string $key, string $op, $value ) : self
6153
        {
6154
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
36✔
6155

6156
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
48✔
6157
                        {
6158
                                switch( $op )
5✔
6159
                                {
6160
                                        case '-':
40✔
6161
                                                $list = (array) $value;
8✔
6162
                                                return $val >= current( $list ) && $val <= end( $list );
8✔
6163
                                        case 'in': return in_array( $val, (array) $value );
32✔
6164
                                        case '<': return $val < $value;
24✔
6165
                                        case '>': return $val > $value;
24✔
6166
                                        case '<=': return $val <= $value;
16✔
6167
                                        case '>=': return $val >= $value;
16✔
6168
                                        case '===': return $val === $value;
16✔
6169
                                        case '!==': return $val !== $value;
16✔
6170
                                        case '!=': return $val != $value;
16✔
6171
                                        default: return $val == $value;
16✔
6172
                                }
6173
                        }
6174

6175
                        return false;
8✔
6176
                } );
48✔
6177
        }
6178

6179

6180
        /**
6181
         * Returns a copy of the map with the element at the given index replaced with the given value.
6182
         *
6183
         * Examples:
6184
         *  $m = Map::from( ['a' => 1] );
6185
         *  $m->with( 2, 'b' );
6186
         *  $m->with( 'a', 2 );
6187
         *
6188
         * Results:
6189
         *  ['a' => 1, 2 => 'b']
6190
         *  ['a' => 2]
6191
         *
6192
         * The original map ($m) stays untouched!
6193
         * This method is a shortcut for calling the copy() and set() methods.
6194
         *
6195
         * @param int|string $key Array key to set or replace
6196
         * @param mixed $value New value for the given key
6197
         * @return self<int|string,mixed> New map
6198
         */
6199
        public function with( $key, $value ) : self
6200
        {
6201
                return ( clone $this )->set( $key, $value );
8✔
6202
        }
6203

6204

6205
        /**
6206
         * Merges the values of all arrays at the corresponding index.
6207
         *
6208
         * Examples:
6209
         *  $en = ['one', 'two', 'three'];
6210
         *  $es = ['uno', 'dos', 'tres'];
6211
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
6212
         *
6213
         * Results:
6214
         *  [
6215
         *    [1, 'one', 'uno'],
6216
         *    [2, 'two', 'dos'],
6217
         *    [3, 'three', 'tres'],
6218
         *  ]
6219
         *
6220
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
6221
         * @return self<int|string,mixed> New map of arrays
6222
         */
6223
        public function zip( ...$arrays ) : self
6224
        {
6225
                $args = array_map( function( $items ) {
6✔
6226
                        return $this->array( $items );
8✔
6227
                }, $arrays );
8✔
6228

6229
                return new static( array_map( null, $this->list(), ...$args ) );
8✔
6230
        }
6231

6232

6233
        /**
6234
         * Returns a plain array of the given elements.
6235
         *
6236
         * @param mixed $elements List of elements or single value
6237
         * @return array<int|string,mixed> Plain array
6238
         */
6239
        protected function array( $elements ) : array
6240
        {
6241
                if( is_array( $elements ) ) {
2,056✔
6242
                        return $elements;
1,976✔
6243
                }
6244

6245
                if( $elements instanceof \Closure ) {
312✔
UNCOV
6246
                        return (array) $elements();
×
6247
                }
6248

6249
                if( $elements instanceof \Aimeos\Map ) {
312✔
6250
                        return $elements->toArray();
184✔
6251
                }
6252

6253
                if( is_iterable( $elements ) ) {
136✔
6254
                        return iterator_to_array( $elements, true );
24✔
6255
                }
6256

6257
                return $elements !== null ? [$elements] : [];
112✔
6258
        }
6259

6260

6261
        /**
6262
         * Flattens a multi-dimensional array or map into a single level array.
6263
         *
6264
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6265
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
6266
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6267
         */
6268
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
6269
        {
6270
                foreach( $entries as $key => $entry )
40✔
6271
                {
6272
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
6273
                                $this->kflatten( $entry, $result, $depth - 1 );
40✔
6274
                        } else {
6275
                                $result[$key] = $entry;
40✔
6276
                        }
6277
                }
6278
        }
10✔
6279

6280

6281
        /**
6282
         * Returns a reference to the array of elements
6283
         *
6284
         * @return array Reference to the array of elements
6285
         */
6286
        protected function &list() : array
6287
        {
6288
                if( !is_array( $this->list ) ) {
2,912✔
UNCOV
6289
                        $this->list = $this->array( $this->list );
×
6290
                }
6291

6292
                return $this->list;
2,912✔
6293
        }
6294

6295

6296
        /**
6297
         * Returns a closure that retrieves the value for the passed key
6298
         *
6299
         * @param \Closure|string|null $key Closure or key (e.g. "key1/key2/key3") to retrieve the value for
6300
         * @return \Closure Closure that retrieves the value for the passed key
6301
         */
6302
        protected function mapper( $key = null ) : \Closure
6303
        {
6304
                if( $key instanceof \Closure ) {
112✔
6305
                        return $key;
48✔
6306
                }
6307

6308
                $parts = $key ? explode( $this->sep, (string) $key ) : [];
64✔
6309

6310
                return function( $item ) use ( $parts ) {
48✔
6311
                        return $this->val( $item, $parts );
64✔
6312
                };
64✔
6313
        }
6314

6315

6316
        /**
6317
         * Flattens a multi-dimensional array or map into a single level array.
6318
         *
6319
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6320
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
6321
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6322
         */
6323
        protected function nflatten( iterable $entries, array &$result, int $depth ) : void
6324
        {
6325
                foreach( $entries as $entry )
40✔
6326
                {
6327
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
6328
                                $this->nflatten( $entry, $result, $depth - 1 );
32✔
6329
                        } else {
6330
                                $result[] = $entry;
40✔
6331
                        }
6332
                }
6333
        }
10✔
6334

6335

6336
        /**
6337
         * Flattens a multi-dimensional array or map into an array with joined keys.
6338
         *
6339
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
6340
         * @param array<int|string,mixed> $result Will contain joined key/value pairs from the multi-dimensional arrays afterwards
6341
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
6342
         * @param string $path Path prefix of the current key
6343
         */
6344
        protected function rflatten( iterable $entries, array &$result, int $depth, string $path = '' ) : void
6345
        {
6346
                foreach( $entries as $key => $entry )
8✔
6347
                {
6348
                        if( is_iterable( $entry ) && $depth > 0 ) {
8✔
6349
                                $this->rflatten( $entry, $result, $depth - 1, $path . $key . $this->sep );
8✔
6350
                        } else {
6351
                                $result[$path . $key] = $entry;
8✔
6352
                        }
6353
                }
6354
        }
2✔
6355

6356

6357
        /**
6358
         * Returns the position of the first element that doesn't match the condition
6359
         *
6360
         * @param iterable<int|string,mixed> $list List of elements to check
6361
         * @param \Closure $callback Closure with ($item, $key) arguments to check the condition
6362
         * @return int Position of the first element that doesn't match the condition
6363
         */
6364
        protected function until( iterable $list, \Closure $callback ) : int
6365
        {
6366
                $idx = 0;
16✔
6367

6368
                foreach( $list as $key => $item )
16✔
6369
                {
6370
                        if( !$callback( $item, $key ) ) {
16✔
6371
                                break;
16✔
6372
                        }
6373

6374
                        ++$idx;
16✔
6375
                }
6376

6377
                return $idx;
16✔
6378
        }
6379

6380

6381
        /**
6382
         * Returns a configuration value from an array.
6383
         *
6384
         * @param array<mixed>|object $entry The array or object to look at
6385
         * @param array<string> $parts Path parts to look for inside the array or object
6386
         * @return mixed Found value or null if no value is available
6387
         */
6388
        protected function val( $entry, array $parts )
6389
        {
6390
                foreach( $parts as $part )
376✔
6391
                {
6392
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
360✔
6393
                                $entry = $entry[$part];
208✔
6394
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
216✔
6395
                                $entry = $entry->{$part};
16✔
6396
                        } else {
6397
                                return null;
220✔
6398
                        }
6399
                }
6400

6401
                return $entry;
232✔
6402
        }
6403

6404

6405
        /**
6406
         * Visits each entry, calls the callback and returns the items in the result argument
6407
         *
6408
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
6409
         * @param array<mixed> $result Numerically indexed list of all visited entries
6410
         * @param int $level Current depth of the nodes in the tree
6411
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
6412
         * @param string $nestKey Key to the children of each entry
6413
         * @param array<mixed>|object|null $parent Parent entry
6414
         */
6415
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, $parent = null ) : void
6416
        {
6417
                foreach( $entries as $key => $entry )
40✔
6418
                {
6419
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
40✔
6420

6421
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
40✔
6422
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
32✔
6423
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
8✔
6424
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
12✔
6425
                        }
6426
                }
6427
        }
10✔
6428
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc