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

aimeos / map / 12156166356

01 Dec 2024 12:16PM UTC coverage: 97.513% (+0.02%) from 97.49%
12156166356

push

github

aimeos
Fixed *sorted() method documentation

745 of 764 relevant lines covered (97.51%)

49.73 hits per line

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

97.5
/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,040✔
53
                $this->list = $elements;
3,040✔
54
        }
760✔
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
                return new static( function() use ( $delimiter, $string, $limit ) {
48✔
207

208
                        if( $delimiter !== '' ) {
64✔
209
                                return explode( $delimiter, $string, $limit );
32✔
210
                        }
211

212
                        $limit = $limit ?: 1;
32✔
213
                        $parts = mb_str_split( $string );
32✔
214

215
                        if( $limit < 1 ) {
32✔
216
                                return array_slice( $parts, 0, $limit );
8✔
217
                        }
218

219
                        if( $limit < count( $parts ) )
24✔
220
                        {
221
                                $result = array_slice( $parts, 0, $limit );
8✔
222
                                $result[] = join( '', array_slice( $parts, $limit ) );
8✔
223
                                return $result;
8✔
224
                        }
225

226
                        return $parts;
16✔
227
                } );
64✔
228
        }
229

230

231
        /**
232
         * Creates a new map instance if the value isn't one already.
233
         *
234
         * Examples:
235
         *  Map::from( [] );
236
         *  Map::from( null );
237
         *  Map::from( 'a' );
238
         *  Map::from( new Map() );
239
         *  Map::from( new ArrayObject() );
240
         *
241
         * Results:
242
         * A new map instance containing the list of elements. In case of an empty
243
         * array or null, the map object will contain an empty list. If a map object
244
         * is passed, it will be returned instead of creating a new instance.
245
         *
246
         * @param mixed $elements List of elements or single element
247
         * @return self<int|string,mixed> New map object
248
         */
249
        public static function from( $elements = [] ) : self
250
        {
251
                if( $elements instanceof self ) {
1,392✔
252
                        return $elements;
24✔
253
                }
254

255
                return new static( $elements );
1,392✔
256
        }
257

258

259
        /**
260
         * Creates a new map instance from a JSON string.
261
         *
262
         * This method creates a lazy Map and the string is decoded after calling
263
         * another method that operates on the Map contents. Thus, the exception in
264
         * case of an error isn't thrown immediately but after calling the next method.
265
         *
266
         * Examples:
267
         *  Map::fromJson( '["a", "b"]' );
268
         *  Map::fromJson( '{"a": "b"}' );
269
         *  Map::fromJson( '""' );
270
         *
271
         * Results:
272
         *  ['a', 'b']
273
         *  ['a' => 'b']
274
         *  ['']
275
         *
276
         * There are several options available for decoding the JSON string:
277
         * {@link https://www.php.net/manual/en/function.json-decode.php}
278
         * The parameter can be a single JSON_* constant or a bitmask of several
279
         * constants combine by bitwise OR (|), e.g.:
280
         *
281
         *  JSON_BIGINT_AS_STRING|JSON_INVALID_UTF8_IGNORE
282
         *
283
         * @param int $options Combination of JSON_* constants
284
         * @return self<int|string,mixed> New map from decoded JSON string
285
         * @throws \RuntimeException If the passed JSON string is invalid
286
         */
287
        public static function fromJson( string $json, int $options = JSON_BIGINT_AS_STRING ) : self
288
        {
289
                return new static( function() use ( $json, $options ) {
24✔
290

291
                        if( ( $result = json_decode( $json, true, 512, $options ) ) !== null ) {
32✔
292
                                return $result;
24✔
293
                        }
294

295
                        throw new \RuntimeException( 'Not a valid JSON string: ' . $json );
8✔
296
                } );
32✔
297
        }
298

299

300
        /**
301
         * Registers a custom method or returns the existing one.
302
         *
303
         * The registed method has access to the class properties if called non-static.
304
         *
305
         * Examples:
306
         *  Map::method( 'foo', function( $arg1, $arg2 ) {
307
         *      return $this->list();
308
         *  } );
309
         *
310
         * Dynamic calls have access to the class properties:
311
         *  Map::from( ['bar'] )->foo( $arg1, $arg2 );
312
         *
313
         * Static calls yield an error because $this->elements isn't available:
314
         *  Map::foo( $arg1, $arg2 );
315
         *
316
         * @param string $method Method name
317
         * @param \Closure|null $fcn Anonymous function or NULL to return the closure if available
318
         * @return \Closure|null Registered anonymous function or NULL if none has been registered
319
         */
320
        public static function method( string $method, ?\Closure $fcn = null ) : ?\Closure
321
        {
322
                if( $fcn ) {
24✔
323
                        self::$methods[$method] = $fcn;
24✔
324
                }
325

326
                return self::$methods[$method] ?? null;
24✔
327
        }
328

329

330
        /**
331
         * Creates a new map by invoking the closure the given number of times.
332
         *
333
         * This method creates a lazy Map and the entries are generated after calling
334
         * another method that operates on the Map contents. Thus, the passed callback
335
         * is not called immediately!
336
         *
337
         * Examples:
338
         *  Map::times( 3, function( $num ) {
339
         *    return $num * 10;
340
         *  } );
341
         *  Map::times( 3, function( $num, &$key ) {
342
         *    $key = $num * 2;
343
         *    return $num * 5;
344
         *  } );
345
         *  Map::times( 2, function( $num ) {
346
         *    return new \stdClass();
347
         *  } );
348
         *
349
         * Results:
350
         *  [0 => 0, 1 => 10, 2 => 20]
351
         *  [0 => 0, 2 => 5, 4 => 10]
352
         *  [0 => new \stdClass(), 1 => new \stdClass()]
353
         *
354
         * @param int $num Number of times the function is called
355
         * @param \Closure $callback Function with (value, key) parameters and returns new value
356
         * @return self<int|string,mixed> New map with the generated elements
357
         */
358
        public static function times( int $num, \Closure $callback ) : self
359
        {
360
                return new static( function() use ( $num, $callback ) {
18✔
361

362
                        $list = [];
24✔
363

364
                        for( $i = 0; $i < $num; $i++ ) {
24✔
365
                                $key = $i;
24✔
366
                                $list[$key] = $callback( $i, $key );
24✔
367
                        }
368

369
                        return $list;
24✔
370
                } );
24✔
371
        }
372

373

374
        /**
375
         * Returns the elements after the given one.
376
         *
377
         * Examples:
378
         *  Map::from( ['a' => 1, 'b' => 0] )->after( 1 );
379
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'b' );
380
         *  Map::from( [0 => 'b', 1 => 'a'] )->after( 'c' );
381
         *  Map::from( ['a', 'c', 'b'] )->after( function( $item, $key ) {
382
         *      return $item >= 'c';
383
         *  } );
384
         *
385
         * Results:
386
         *  ['b' => 0]
387
         *  [1 => 'a']
388
         *  []
389
         *  [2 => 'b']
390
         *
391
         * The keys are preserved using this method.
392
         *
393
         * @param \Closure|int|string $value Value or function with (item, key) parameters
394
         * @return self<int|string,mixed> New map with the elements after the given one
395
         */
396
        public function after( $value ) : self
397
        {
398
                if( ( $pos = $this->pos( $value ) ) === null ) {
32✔
399
                        return new static();
8✔
400
                }
401

402
                return new static( array_slice( $this->list(), $pos + 1, null, true ) );
24✔
403
        }
404

405

406
        /**
407
         * Returns the elements as a plain array.
408
         *
409
         * @return array<int|string,mixed> Plain array
410
         */
411
        public function all() : array
412
        {
413
                return $this->list = $this->array( $this->list );
120✔
414
        }
415

416

417
        /**
418
         * Sorts all elements in reverse order and maintains the key association.
419
         *
420
         * Examples:
421
         *  Map::from( ['b' => 0, 'a' => 1] )->arsort();
422
         *  Map::from( ['a', 'b'] )->arsort();
423
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort();
424
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
425
         *
426
         * Results:
427
         *  ['a' => 1, 'b' => 0]
428
         *  ['b', 'a']
429
         *  [1 => 'b', 0 => 'C']
430
         *  [0 => 'C', 1 => 'b'] // because 'C' -> 'c' and 'c' > 'b'
431
         *
432
         * The parameter modifies how the values are compared. Possible parameter values are:
433
         * - SORT_REGULAR : compare elements normally (don't change types)
434
         * - SORT_NUMERIC : compare elements numerically
435
         * - SORT_STRING : compare elements as strings
436
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
437
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
438
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
439
         *
440
         * The keys are preserved using this method and no new map is created.
441
         *
442
         * @param int $options Sort options for arsort()
443
         * @return self<int|string,mixed> Updated map for fluid interface
444
         */
445
        public function arsort( int $options = SORT_REGULAR ) : self
446
        {
447
                arsort( $this->list(), $options );
32✔
448
                return $this;
32✔
449
        }
450

451

452
        /**
453
         * Sorts a copy of all elements in reverse order and maintains the key association.
454
         *
455
         * Examples:
456
         *  Map::from( ['b' => 0, 'a' => 1] )->arsorted();
457
         *  Map::from( ['a', 'b'] )->arsorted();
458
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsorted();
459
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsorted( SORT_STRING|SORT_FLAG_CASE );
460
         *
461
         * Results:
462
         *  ['a' => 1, 'b' => 0]
463
         *  ['b', 'a']
464
         *  [1 => 'b', 0 => 'C']
465
         *  [0 => 'C', 1 => 'b'] // because 'C' -> 'c' and 'c' > 'b'
466
         *
467
         * The parameter modifies how the values are compared. Possible parameter values are:
468
         * - SORT_REGULAR : compare elements normally (don't change types)
469
         * - SORT_NUMERIC : compare elements numerically
470
         * - SORT_STRING : compare elements as strings
471
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
472
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
473
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
474
         *
475
         * The keys are preserved using this method and a new map is created.
476
         *
477
         * @param int $options Sort options for arsort()
478
         * @return self<int|string,mixed> Updated map for fluid interface
479
         */
480
        public function arsorted( int $options = SORT_REGULAR ) : self
481
        {
482
                return ( clone $this )->arsort( $options );
8✔
483
        }
484

485

486
        /**
487
         * Sorts all elements and maintains the key association.
488
         *
489
         * Examples:
490
         *  Map::from( ['a' => 1, 'b' => 0] )->asort();
491
         *  Map::from( [0 => 'b', 1 => 'a'] )->asort();
492
         *  Map::from( [0 => 'C', 1 => 'b'] )->asort();
493
         *  Map::from( [0 => 'C', 1 => 'b'] )->arsort( SORT_STRING|SORT_FLAG_CASE );
494
         *
495
         * Results:
496
         *  ['b' => 0, 'a' => 1]
497
         *  [1 => 'a', 0 => 'b']
498
         *  [0 => 'C', 1 => 'b'] // because 'C' < 'b'
499
         *  [1 => 'b', 0 => 'C'] // because 'C' -> 'c' and 'c' > 'b'
500
         *
501
         * The parameter modifies how the values are compared. Possible parameter values are:
502
         * - SORT_REGULAR : compare elements normally (don't change types)
503
         * - SORT_NUMERIC : compare elements numerically
504
         * - SORT_STRING : compare elements as strings
505
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
506
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
507
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
508
         *
509
         * The keys are preserved using this method and no new map is created.
510
         *
511
         * @param int $options Sort options for asort()
512
         * @return self<int|string,mixed> Updated map for fluid interface
513
         */
514
        public function asort( int $options = SORT_REGULAR ) : self
515
        {
516
                asort( $this->list(), $options );
32✔
517
                return $this;
32✔
518
        }
519

520

521
        /**
522
         * Sorts a copy of all elements and maintains the key association.
523
         *
524
         * Examples:
525
         *  Map::from( ['a' => 1, 'b' => 0] )->asorted();
526
         *  Map::from( [0 => 'b', 1 => 'a'] )->asorted();
527
         *  Map::from( [0 => 'C', 1 => 'b'] )->asorted();
528
         *  Map::from( [0 => 'C', 1 => 'b'] )->asorted( SORT_STRING|SORT_FLAG_CASE );
529
         *
530
         * Results:
531
         *  ['b' => 0, 'a' => 1]
532
         *  [1 => 'a', 0 => 'b']
533
         *  [0 => 'C', 1 => 'b'] // because 'C' < 'b'
534
         *  [1 => 'b', 0 => 'C'] // because 'C' -> 'c' and 'c' > 'b'
535
         *
536
         * The parameter modifies how the values are compared. Possible parameter values are:
537
         * - SORT_REGULAR : compare elements normally (don't change types)
538
         * - SORT_NUMERIC : compare elements numerically
539
         * - SORT_STRING : compare elements as strings
540
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
541
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
542
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
543
         *
544
         * The keys are preserved using this method and a new map is created.
545
         *
546
         * @param int $options Sort options for asort()
547
         * @return self<int|string,mixed> Updated map for fluid interface
548
         */
549
        public function asorted( int $options = SORT_REGULAR ) : self
550
        {
551
                return ( clone $this )->asort( $options );
8✔
552
        }
553

554

555
        /**
556
         * Returns the value at the given position.
557
         *
558
         * Examples:
559
         *  Map::from( [1, 3, 5] )->at( 0 );
560
         *  Map::from( [1, 3, 5] )->at( 1 );
561
         *  Map::from( [1, 3, 5] )->at( -1 );
562
         *  Map::from( [1, 3, 5] )->at( 3 );
563
         *
564
         * Results:
565
         * The first line will return "1", the second one "3", the third one "5" and
566
         * the last one NULL.
567
         *
568
         * The position starts from zero and a position of "0" returns the first element
569
         * of the map, "1" the second and so on. If the position is negative, the
570
         * sequence will start from the end of the map.
571
         *
572
         * @param int $pos Position of the value in the map
573
         * @return mixed|null Value at the given position or NULL if no value is available
574
         */
575
        public function at( int $pos )
576
        {
577
                $pair = array_slice( $this->list(), $pos, 1 );
8✔
578
                return !empty( $pair ) ? current( $pair ) : null;
8✔
579
        }
580

581

582
        /**
583
         * Returns the average of all integer and float values in the map.
584
         *
585
         * Examples:
586
         *  Map::from( [1, 3, 5] )->avg();
587
         *  Map::from( [1, null, 5] )->avg();
588
         *  Map::from( [1, 'sum', 5] )->avg();
589
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->avg( 'p' );
590
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->avg( 'i/p' );
591
         *  Map::from( [30, 50, 10] )->avg( fn( $val, $key ) => $val < 50 );
592
         *
593
         * Results:
594
         * The first and second line will return "3", the third one "2", the forth
595
         * one "30", the fifth one "40" and the last one "20".
596
         *
597
         * NULL values are treated as 0, non-numeric values will generate an error.
598
         *
599
         * NULL values are treated as 0, non-numeric values will generate an error.
600
         *
601
         * This does also work for multi-dimensional arrays by passing the keys
602
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
603
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
604
         * public properties of objects or objects implementing __isset() and __get() methods.
605
         *
606
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to compute the average for
607
         * @return float Average of all elements or 0 if there are no elements in the map
608
         */
609
        public function avg( $col = null ) : float
610
        {
611
                if( $col instanceof \Closure ) {
24✔
612
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
613
                } elseif( is_string( $col ) ) {
16✔
614
                        $vals = $this->col( $col )->toArray();
8✔
615
                } elseif( is_null( $col ) ) {
8✔
616
                        $vals = $this->list();
8✔
617
                } else {
618
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
619
                }
620

621
                $cnt = count( $vals );
24✔
622
                return $cnt > 0 ? array_sum( $vals ) / $cnt : 0;
24✔
623
        }
624

625

626
        /**
627
         * Returns the elements before the given one.
628
         *
629
         * Examples:
630
         *  Map::from( ['a' => 1, 'b' => 0] )->before( 0 );
631
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'a' );
632
         *  Map::from( [0 => 'b', 1 => 'a'] )->before( 'b' );
633
         *  Map::from( ['a', 'c', 'b'] )->before( function( $item, $key ) {
634
         *      return $key >= 1;
635
         *  } );
636
         *
637
         * Results:
638
         *  ['a' => 1]
639
         *  [0 => 'b']
640
         *  []
641
         *  [0 => 'a']
642
         *
643
         * The keys are preserved using this method.
644
         *
645
         * @param \Closure|int|string $value Value or function with (item, key) parameters
646
         * @return self<int|string,mixed> New map with the elements before the given one
647
         */
648
        public function before( $value ) : self
649
        {
650
                return new static( array_slice( $this->list(), 0, $this->pos( $value ), true ) );
32✔
651
        }
652

653

654
        /**
655
         * Returns an element by key and casts it to boolean if possible.
656
         *
657
         * Examples:
658
         *  Map::from( ['a' => true] )->bool( 'a' );
659
         *  Map::from( ['a' => '1'] )->bool( 'a' );
660
         *  Map::from( ['a' => 1.1] )->bool( 'a' );
661
         *  Map::from( ['a' => '10'] )->bool( 'a' );
662
         *  Map::from( ['a' => 'abc'] )->bool( 'a' );
663
         *  Map::from( ['a' => ['b' => ['c' => true]]] )->bool( 'a/b/c' );
664
         *  Map::from( [] )->bool( 'c', function() { return rand( 1, 2 ); } );
665
         *  Map::from( [] )->bool( 'a', true );
666
         *
667
         *  Map::from( [] )->bool( 'b' );
668
         *  Map::from( ['b' => ''] )->bool( 'b' );
669
         *  Map::from( ['b' => null] )->bool( 'b' );
670
         *  Map::from( ['b' => [true]] )->bool( 'b' );
671
         *  Map::from( ['b' => resource] )->bool( 'b' );
672
         *  Map::from( ['b' => new \stdClass] )->bool( 'b' );
673
         *
674
         *  Map::from( [] )->bool( 'c', new \Exception( 'error' ) );
675
         *
676
         * Results:
677
         * The first eight examples will return TRUE while the 9th to 14th example
678
         * returns FALSE. The last example will throw an exception.
679
         *
680
         * This does also work for multi-dimensional arrays by passing the keys
681
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
682
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
683
         * public properties of objects or objects implementing __isset() and __get() methods.
684
         *
685
         * @param int|string $key Key or path to the requested item
686
         * @param mixed $default Default value if key isn't found (will be casted to bool)
687
         * @return bool Value from map or default value
688
         */
689
        public function bool( $key, $default = false ) : bool
690
        {
691
                return (bool) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
692
        }
693

694

695
        /**
696
         * Calls the given method on all items and returns the result.
697
         *
698
         * This method can call methods on the map entries that are also implemented
699
         * by the map object itself and are therefore not reachable when using the
700
         * magic __call() method.
701
         *
702
         * Examples:
703
         *  $item = new MyClass(); // implements methods get() and toArray()
704
         *  Map::from( [$item, $item] )->call( 'get', ['myprop'] );
705
         *  Map::from( [$item, $item] )->call( 'toArray' );
706
         *
707
         * Results:
708
         * The first example will return ['...', '...'] while the second one returns [[...], [...]].
709
         *
710
         * If some entries are not objects, they will be skipped. The map keys from the
711
         * original map are preserved in the returned map.
712
         *
713
         * @param string $name Method name
714
         * @param array<mixed> $params List of parameters
715
         * @return self<int|string,mixed> New map with results from all elements
716
         */
717
        public function call( string $name, array $params = [] ) : self
718
        {
719
                $result = [];
8✔
720

721
                foreach( $this->list() as $key => $item )
8✔
722
                {
723
                        if( is_object( $item ) ) {
8✔
724
                                $result[$key] = $item->{$name}( ...$params );
8✔
725
                        }
726
                }
727

728
                return new static( $result );
8✔
729
        }
730

731

732
        /**
733
         * Casts all entries to the passed type.
734
         *
735
         * Examples:
736
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast();
737
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'bool' );
738
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'int' );
739
         *  Map::from( [true, 1, 1.0, 'yes'] )->cast( 'float' );
740
         *  Map::from( [new stdClass, new stdClass] )->cast( 'array' );
741
         *  Map::from( [[], []] )->cast( 'object' );
742
         *
743
         * Results:
744
         * The examples will return (in this order):
745
         * ['1', '1', '1.0', 'yes']
746
         * [true, true, true, true]
747
         * [1, 1, 1, 0]
748
         * [1.0, 1.0, 1.0, 0.0]
749
         * [[], []]
750
         * [new stdClass, new stdClass]
751
         *
752
         * Casting arrays and objects to scalar values won't return anything useful!
753
         *
754
         * @param string $type Type to cast the values to ("string", "bool", "int", "float", "array", "object")
755
         * @return self<int|string,mixed> Updated map with casted elements
756
         */
757
        public function cast( string $type = 'string' ) : self
758
        {
759
                foreach( $this->list() as &$item )
8✔
760
                {
761
                        switch( $type )
1✔
762
                        {
763
                                case 'bool': $item = (bool) $item; break;
8✔
764
                                case 'int': $item = (int) $item; break;
8✔
765
                                case 'float': $item = (float) $item; break;
8✔
766
                                case 'string': $item = (string) $item; break;
8✔
767
                                case 'array': $item = (array) $item; break;
8✔
768
                                case 'object': $item = (object) $item; break;
8✔
769
                        }
770
                }
771

772
                return $this;
8✔
773
        }
774

775

776
        /**
777
         * Chunks the map into arrays with the given number of elements.
778
         *
779
         * Examples:
780
         *  Map::from( [0, 1, 2, 3, 4] )->chunk( 3 );
781
         *  Map::from( ['a' => 0, 'b' => 1, 'c' => 2] )->chunk( 2 );
782
         *
783
         * Results:
784
         *  [[0, 1, 2], [3, 4]]
785
         *  [['a' => 0, 'b' => 1], ['c' => 2]]
786
         *
787
         * The last chunk may contain less elements than the given number.
788
         *
789
         * The sub-arrays of the returned map are plain PHP arrays. If you need Map
790
         * objects, then wrap them with Map::from() when you iterate over the map.
791
         *
792
         * @param int $size Maximum size of the sub-arrays
793
         * @param bool $preserve Preserve keys in new map
794
         * @return self<int|string,mixed> New map with elements chunked in sub-arrays
795
         * @throws \InvalidArgumentException If size is smaller than 1
796
         */
797
        public function chunk( int $size, bool $preserve = false ) : self
798
        {
799
                if( $size < 1 ) {
24✔
800
                        throw new \InvalidArgumentException( 'Chunk size must be greater or equal than 1' );
8✔
801
                }
802

803
                return new static( array_chunk( $this->list(), $size, $preserve ) );
16✔
804
        }
805

806

807
        /**
808
         * Removes all elements from the current map.
809
         *
810
         * @return self<int|string,mixed> Updated map for fluid interface
811
         */
812
        public function clear() : self
813
        {
814
                $this->list = [];
32✔
815
                return $this;
32✔
816
        }
817

818

819
        /**
820
         * Clones the map and all objects within.
821
         *
822
         * Examples:
823
         *  Map::from( [new \stdClass, new \stdClass] )->clone();
824
         *
825
         * Results:
826
         *   [new \stdClass, new \stdClass]
827
         *
828
         * The objects within the Map are NOT the same as before but new cloned objects.
829
         * This is different to copy(), which doesn't clone the objects within.
830
         *
831
         * The keys are preserved using this method.
832
         *
833
         * @return self<int|string,mixed> New map with cloned objects
834
         */
835
        public function clone() : self
836
        {
837
                $list = [];
8✔
838

839
                foreach( $this->list() as $key => $item ) {
8✔
840
                        $list[$key] = is_object( $item ) ? clone $item : $item;
8✔
841
                }
842

843
                return new static( $list );
8✔
844
        }
845

846

847
        /**
848
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
849
         *
850
         * Examples:
851
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val' );
852
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( 'val', 'id' );
853
         *  Map::from( [['id' => 'i1', 'val' => 'v1'], ['id' => 'i2', 'val' => 'v2']] )->col( null, 'id' );
854
         *  Map::from( [['id' => 'ix', 'val' => 'v1'], ['id' => 'ix', 'val' => 'v2']] )->col( null, 'id' );
855
         *  Map::from( [['foo' => ['bar' => 'one', 'baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
856
         *  Map::from( [['foo' => ['bar' => 'one']]] )->col( 'foo/baz', 'foo/bar' );
857
         *  Map::from( [['foo' => ['baz' => 'two']]] )->col( 'foo/baz', 'foo/bar' );
858
         *
859
         * Results:
860
         *  ['v1', 'v2']
861
         *  ['i1' => 'v1', 'i2' => 'v2']
862
         *  ['i1' => ['id' => 'i1', 'val' => 'v1'], 'i2' => ['id' => 'i2', 'val' => 'v2']]
863
         *  ['ix' => ['id' => 'ix', 'val' => 'v2']]
864
         *  ['one' => 'two']
865
         *  ['one' => null]
866
         *  ['two']
867
         *
868
         * If $indexcol is omitted, it's value is NULL or not set, the result will be indexed from 0-n.
869
         * Items with the same value for $indexcol will overwrite previous items and only the last
870
         * one will be part of the resulting map.
871
         *
872
         * This does also work to map values from multi-dimensional arrays by passing the keys
873
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
874
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
875
         * public properties of objects or objects implementing __isset() and __get() methods.
876
         *
877
         * @param string|null $valuecol Name or path of the value property
878
         * @param string|null $indexcol Name or path of the index property
879
         * @return self<int|string,mixed> New map with mapped entries
880
         */
881
        public function col( ?string $valuecol = null, ?string $indexcol = null ) : self
882
        {
883
                $vparts = explode( $this->sep, (string) $valuecol );
136✔
884
                $iparts = explode( $this->sep, (string) $indexcol );
136✔
885

886
                if( count( $vparts ) === 1 && count( $iparts ) === 1 ) {
136✔
887
                        return new static( array_column( $this->list(), $valuecol, $indexcol ) );
96✔
888
                }
889

890
                $list = [];
72✔
891

892
                foreach( $this->list() as $item )
72✔
893
                {
894
                        $v = $valuecol ? $this->val( $item, $vparts ) : $item;
72✔
895

896
                        if( $indexcol !== null && ( $key = $this->val( $item, $iparts ) ) !== null ) {
72✔
897
                                $list[(string) $key] = $v;
24✔
898
                        } else {
899
                                $list[] = $v;
51✔
900
                        }
901
                }
902

903
                return new static( $list );
72✔
904
        }
905

906

907
        /**
908
         * Collapses all sub-array elements recursively to a new map overwriting existing keys.
909
         *
910
         * Examples:
911
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['c' => 2, 'd' => 3]] )->collapse();
912
         *  Map::from( [0 => ['a' => 0, 'b' => 1], 1 => ['a' => 2]] )->collapse();
913
         *  Map::from( [0 => [0 => 0, 1 => 1], 1 => [0 => ['a' => 2, 0 => 3], 1 => 4]] )->collapse();
914
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => [0 => ['b' => 2, 0 => 3], 1 => 4]] )->collapse( 1 );
915
         *  Map::from( [0 => [0 => 0, 'a' => 1], 1 => Map::from( [0 => ['b' => 2, 0 => 3], 1 => 4] )] )->collapse();
916
         *
917
         * Results:
918
         *  ['a' => 0, 'b' => 1, 'c' => 2, 'd' => 3]
919
         *  ['a' => 2, 'b' => 1]
920
         *  [0 => 3, 1 => 4, 'a' => 2]
921
         *  [0 => ['b' => 2, 0 => 3], 1 => 4, 'a' => 1]
922
         *  [0 => 3, 'a' => 1, 'b' => 2, 1 => 4]
923
         *
924
         * The keys are preserved and already existing elements will be overwritten.
925
         * This is also true for numeric keys! A value smaller than 1 for depth will
926
         * return the same map elements. Collapsing does also work if elements
927
         * implement the "Traversable" interface (which the Map object does).
928
         *
929
         * This method is similar than flat() but replaces already existing elements.
930
         *
931
         * @param int|null $depth Number of levels to collapse for multi-dimensional arrays or NULL for all
932
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
933
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
934
         */
935
        public function collapse( ?int $depth = null ) : self
936
        {
937
                if( $depth < 0 ) {
48✔
938
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
8✔
939
                }
940

941
                $result = [];
40✔
942
                $this->kflatten( $this->list(), $result, $depth ?? 0x7fffffff );
40✔
943
                return new static( $result );
40✔
944
        }
945

946

947
        /**
948
         * Combines the values of the map as keys with the passed elements as values.
949
         *
950
         * Examples:
951
         *  Map::from( ['name', 'age'] )->combine( ['Tom', 29] );
952
         *
953
         * Results:
954
         *  ['name' => 'Tom', 'age' => 29]
955
         *
956
         * @param iterable<int|string,mixed> $values Values of the new map
957
         * @return self<int|string,mixed> New map
958
         */
959
        public function combine( iterable $values ) : self
960
        {
961
                return new static( array_combine( $this->list(), $this->array( $values ) ) );
8✔
962
        }
963

964

965
        /**
966
         * Compares the value against all map elements.
967
         *
968
         * Examples:
969
         *  Map::from( ['foo', 'bar'] )->compare( 'foo' );
970
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo', false );
971
         *  Map::from( [123, 12.3] )->compare( '12.3' );
972
         *  Map::from( [false, true] )->compare( '1' );
973
         *  Map::from( ['foo', 'bar'] )->compare( 'Foo' );
974
         *  Map::from( ['foo', 'bar'] )->compare( 'baz' );
975
         *  Map::from( [new \stdClass(), 'bar'] )->compare( 'foo' );
976
         *
977
         * Results:
978
         * The first four examples return TRUE, the last three examples will return FALSE.
979
         *
980
         * All scalar values (bool, float, int and string) are casted to string values before
981
         * comparing to the given value. Non-scalar values in the map are ignored.
982
         *
983
         * @param string $value Value to compare map elements to
984
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
985
         * @return bool TRUE If at least one element matches, FALSE if value is not in map
986
         */
987
        public function compare( string $value, bool $case = true ) : bool
988
        {
989
                $fcn = $case ? 'strcmp' : 'strcasecmp';
8✔
990

991
                foreach( $this->list() as $item )
8✔
992
                {
993
                        if( is_scalar( $item ) && !$fcn( (string) $item, $value ) ) {
8✔
994
                                return true;
8✔
995
                        }
996
                }
997

998
                return false;
8✔
999
        }
1000

1001

1002
        /**
1003
         * Pushs all of the given elements onto the map with new keys without creating a new map.
1004
         *
1005
         * Examples:
1006
         *  Map::from( ['foo'] )->concat( new Map( ['bar'] ));
1007
         *
1008
         * Results:
1009
         *  ['foo', 'bar']
1010
         *
1011
         * The keys of the passed elements are NOT preserved!
1012
         *
1013
         * @param iterable<int|string,mixed> $elements List of elements
1014
         * @return self<int|string,mixed> Updated map for fluid interface
1015
         */
1016
        public function concat( iterable $elements ) : self
1017
        {
1018
                $this->list();
16✔
1019

1020
                foreach( $elements as $item ) {
16✔
1021
                        $this->list[] = $item;
16✔
1022
                }
1023

1024
                return $this;
16✔
1025
        }
1026

1027

1028
        /**
1029
         * Determines if an item exists in the map.
1030
         *
1031
         * This method combines the power of the where() method with some() to check
1032
         * if the map contains at least one of the passed values or conditions.
1033
         *
1034
         * Examples:
1035
         *  Map::from( ['a', 'b'] )->contains( 'a' );
1036
         *  Map::from( ['a', 'b'] )->contains( ['a', 'c'] );
1037
         *  Map::from( ['a', 'b'] )->contains( function( $item, $key ) {
1038
         *    return $item === 'a'
1039
         *  } );
1040
         *  Map::from( [['type' => 'name']] )->contains( 'type', 'name' );
1041
         *  Map::from( [['type' => 'name']] )->contains( 'type', '==', 'name' );
1042
         *
1043
         * Results:
1044
         * All method calls will return TRUE because at least "a" is included in the
1045
         * map or there's a "type" key with a value "name" like in the last two
1046
         * examples.
1047
         *
1048
         * Check the where() method for available operators.
1049
         *
1050
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
1051
         * @param string|null $op Operator used for comparison
1052
         * @param mixed $value Value used for comparison
1053
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
1054
         */
1055
        public function contains( $key, ?string $operator = null, $value = null ) : bool
1056
        {
1057
                if( $operator === null ) {
16✔
1058
                        return $this->some( $key );
8✔
1059
                }
1060

1061
                if( $value === null ) {
8✔
1062
                        return !$this->where( $key, '==', $operator )->isEmpty();
8✔
1063
                }
1064

1065
                return !$this->where( $key, $operator, $value )->isEmpty();
8✔
1066
        }
1067

1068

1069
        /**
1070
         * Creates a new map with the same elements.
1071
         *
1072
         * Both maps share the same array until one of the map objects modifies the
1073
         * array. Then, the array is copied and the copy is modfied (copy on write).
1074
         *
1075
         * @return self<int|string,mixed> New map
1076
         */
1077
        public function copy() : self
1078
        {
1079
                return clone $this;
24✔
1080
        }
1081

1082

1083
        /**
1084
         * Counts the total number of elements in the map.
1085
         *
1086
         * @return int Number of elements
1087
         */
1088
        public function count() : int
1089
        {
1090
                return count( $this->list() );
72✔
1091
        }
1092

1093

1094
        /**
1095
         * Counts how often the same values are in the map.
1096
         *
1097
         * Examples:
1098
         *  Map::from( [1, 'foo', 2, 'foo', 1] )->countBy();
1099
         *  Map::from( [1.11, 3.33, 3.33, 9.99] )->countBy();
1100
         *  Map::from( ['a@gmail.com', 'b@yahoo.com', 'c@gmail.com'] )->countBy( function( $email ) {
1101
         *    return substr( strrchr( $email, '@' ), 1 );
1102
         *  } );
1103
         *
1104
         * Results:
1105
         *  [1 => 2, 'foo' => 2, 2 => 1]
1106
         *  ['1.11' => 1, '3.33' => 2, '9.99' => 1]
1107
         *  ['gmail.com' => 2, 'yahoo.com' => 1]
1108
         *
1109
         * Counting values does only work for integers and strings because these are
1110
         * the only types allowed as array keys. All elements are casted to strings
1111
         * if no callback is passed. Custom callbacks need to make sure that only
1112
         * string or integer values are returned!
1113
         *
1114
         * @param  callable|null $callback Function with (value, key) parameters which returns the value to use for counting
1115
         * @return self<int|string,mixed> New map with values as keys and their count as value
1116
         */
1117
        public function countBy( ?callable $callback = null ) : self
1118
        {
1119
                $callback = $callback ?: function( $value ) {
18✔
1120
                        return (string) $value;
16✔
1121
                };
24✔
1122

1123
                return new static( array_count_values( array_map( $callback, $this->list() ) ) );
24✔
1124
        }
1125

1126

1127
        /**
1128
         * Dumps the map content and terminates the script.
1129
         *
1130
         * The dd() method is very helpful to see what are the map elements passed
1131
         * between two map methods in a method call chain. It stops execution of the
1132
         * script afterwards to avoid further output.
1133
         *
1134
         * Examples:
1135
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->sort()->dd();
1136
         *
1137
         * Results:
1138
         *  Array
1139
         *  (
1140
         *      [0] => bar
1141
         *      [1] => foo
1142
         *  )
1143
         *
1144
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1145
         */
1146
        public function dd( ?callable $callback = null ) : void
1147
        {
1148
                $this->dump( $callback );
×
1149
                exit( 1 );
×
1150
        }
1151

1152

1153
        /**
1154
         * Returns the keys/values in the map whose values are not present in the passed elements in a new map.
1155
         *
1156
         * Examples:
1157
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->diff( ['bar'] );
1158
         *
1159
         * Results:
1160
         *  ['a' => 'foo']
1161
         *
1162
         * If a callback is passed, the given function will be used to compare the values.
1163
         * The function must accept two parameters (value A and B) and must return
1164
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
1165
         * greater than value B. Both, a method name and an anonymous function can be passed:
1166
         *
1167
         *  Map::from( [0 => 'a'] )->diff( [0 => 'A'], 'strcasecmp' );
1168
         *  Map::from( ['b' => 'a'] )->diff( ['B' => 'A'], 'strcasecmp' );
1169
         *  Map::from( ['b' => 'a'] )->diff( ['c' => 'A'], function( $valA, $valB ) {
1170
         *      return strtolower( $valA ) <=> strtolower( $valB );
1171
         *  } );
1172
         *
1173
         * All examples will return an empty map because both contain the same values
1174
         * when compared case insensitive.
1175
         *
1176
         * The keys are preserved using this method.
1177
         *
1178
         * @param iterable<int|string,mixed> $elements List of elements
1179
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
1180
         * @return self<int|string,mixed> New map
1181
         */
1182
        public function diff( iterable $elements, ?callable $callback = null ) : self
1183
        {
1184
                if( $callback ) {
24✔
1185
                        return new static( array_udiff( $this->list(), $this->array( $elements ), $callback ) );
8✔
1186
                }
1187

1188
                return new static( array_diff( $this->list(), $this->array( $elements ) ) );
24✔
1189
        }
1190

1191

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

1229
                return new static( array_diff_assoc( $this->list(), $this->array( $elements ) ) );
16✔
1230
        }
1231

1232

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

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

1272

1273
        /**
1274
         * Dumps the map content using the given function (print_r by default).
1275
         *
1276
         * The dump() method is very helpful to see what are the map elements passed
1277
         * between two map methods in a method call chain.
1278
         *
1279
         * Examples:
1280
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->dump()->asort()->dump( 'var_dump' );
1281
         *
1282
         * Results:
1283
         *  Array
1284
         *  (
1285
         *      [a] => foo
1286
         *      [b] => bar
1287
         *  )
1288
         *  array(1) {
1289
         *    ["b"]=>
1290
         *    string(3) "bar"
1291
         *    ["a"]=>
1292
         *    string(3) "foo"
1293
         *  }
1294
         *
1295
         * @param callable|null $callback Function receiving the map elements as parameter (optional)
1296
         * @return self<int|string,mixed> Same map for fluid interface
1297
         */
1298
        public function dump( ?callable $callback = null ) : self
1299
        {
1300
                $callback ? $callback( $this->list() ) : print_r( $this->list() );
8✔
1301
                return $this;
8✔
1302
        }
1303

1304

1305
        /**
1306
         * Returns the duplicate values from the map.
1307
         *
1308
         * For nested arrays, you have to pass the name of the column of the nested
1309
         * array which should be used to check for duplicates.
1310
         *
1311
         * Examples:
1312
         *  Map::from( [1, 2, '1', 3] )->duplicates()
1313
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->duplicates( 'p' )
1314
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->duplicates( 'i/p' )
1315
         *
1316
         * Results:
1317
         *  [2 => '1']
1318
         *  [1 => ['p' => 1]]
1319
         *  [1 => ['i' => ['p' => '1']]]
1320
         *
1321
         * This does also work for multi-dimensional arrays by passing the keys
1322
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1323
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1324
         * public properties of objects or objects implementing __isset() and __get() methods.
1325
         *
1326
         * The keys are preserved using this method.
1327
         *
1328
         * @param string|null $key Key or path of the nested array or object to check for
1329
         * @return self<int|string,mixed> New map
1330
         */
1331
        public function duplicates( ?string $key = null ) : self
1332
        {
1333
                $list = $this->list();
24✔
1334
                $items = ( $key !== null ? $this->col( $key )->toArray() : $list );
24✔
1335

1336
                return new static( array_diff_key( $list, array_unique( $items ) ) );
24✔
1337
        }
1338

1339

1340
        /**
1341
         * Executes a callback over each entry until FALSE is returned.
1342
         *
1343
         * Examples:
1344
         *  $result = [];
1345
         *  Map::from( [0 => 'a', 1 => 'b'] )->each( function( $value, $key ) use ( &$result ) {
1346
         *      $result[$key] = strtoupper( $value );
1347
         *      return false;
1348
         *  } );
1349
         *
1350
         * The $result array will contain [0 => 'A'] because FALSE is returned
1351
         * after the first entry and all other entries are then skipped.
1352
         *
1353
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1354
         * @return self<int|string,mixed> Same map for fluid interface
1355
         */
1356
        public function each( \Closure $callback ) : self
1357
        {
1358
                foreach( $this->list() as $key => $item )
16✔
1359
                {
1360
                        if( $callback( $item, $key ) === false ) {
16✔
1361
                                break;
9✔
1362
                        }
1363
                }
1364

1365
                return $this;
16✔
1366
        }
1367

1368

1369
        /**
1370
         * Determines if the map is empty or not.
1371
         *
1372
         * Examples:
1373
         *  Map::from( [] )->empty();
1374
         *  Map::from( ['a'] )->empty();
1375
         *
1376
         * Results:
1377
         *  The first example returns TRUE while the second returns FALSE
1378
         *
1379
         * The method is equivalent to isEmpty().
1380
         *
1381
         * @return bool TRUE if map is empty, FALSE if not
1382
         */
1383
        public function empty() : bool
1384
        {
1385
                return empty( $this->list() );
16✔
1386
        }
1387

1388

1389
        /**
1390
         * Tests if the passed elements are equal to the elements in the map.
1391
         *
1392
         * Examples:
1393
         *  Map::from( ['a'] )->equals( ['a', 'b'] );
1394
         *  Map::from( ['a', 'b'] )->equals( ['b'] );
1395
         *  Map::from( ['a', 'b'] )->equals( ['b', 'a'] );
1396
         *
1397
         * Results:
1398
         * The first and second example will return FALSE, the third example will return TRUE
1399
         *
1400
         * The method differs to is() in the fact that it doesn't care about the keys
1401
         * by default. The elements are only loosely compared and the keys are ignored.
1402
         *
1403
         * Values are compared by their string values:
1404
         * (string) $item1 === (string) $item2
1405
         *
1406
         * @param iterable<int|string,mixed> $elements List of elements to test against
1407
         * @return bool TRUE if both are equal, FALSE if not
1408
         */
1409
        public function equals( iterable $elements ) : bool
1410
        {
1411
                $list = $this->list();
48✔
1412
                $elements = $this->array( $elements );
48✔
1413

1414
                return array_diff( $list, $elements ) === [] && array_diff( $elements, $list ) === [];
48✔
1415
        }
1416

1417

1418
        /**
1419
         * Verifies that all elements pass the test of the given callback.
1420
         *
1421
         * Examples:
1422
         *  Map::from( [0 => 'a', 1 => 'b'] )->every( function( $value, $key ) {
1423
         *      return is_string( $value );
1424
         *  } );
1425
         *
1426
         *  Map::from( [0 => 'a', 1 => 100] )->every( function( $value, $key ) {
1427
         *      return is_string( $value );
1428
         *  } );
1429
         *
1430
         * The first example will return TRUE because all values are a string while
1431
         * the second example will return FALSE.
1432
         *
1433
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1434
         * @return bool True if all elements pass the test, false if if fails for at least one element
1435
         */
1436
        public function every( \Closure $callback ) : bool
1437
        {
1438
                foreach( $this->list() as $key => $item )
8✔
1439
                {
1440
                        if( $callback( $item, $key ) === false ) {
8✔
1441
                                return false;
8✔
1442
                        }
1443
                }
1444

1445
                return true;
8✔
1446
        }
1447

1448

1449
        /**
1450
         * Returns a new map without the passed element keys.
1451
         *
1452
         * Examples:
1453
         *  Map::from( ['a' => 1, 'b' => 2, 'c' => 3] )->except( 'b' );
1454
         *  Map::from( [1 => 'a', 2 => 'b', 3 => 'c'] )->except( [1, 3] );
1455
         *
1456
         * Results:
1457
         *  ['a' => 1, 'c' => 3]
1458
         *  [2 => 'b']
1459
         *
1460
         * The keys in the result map are preserved.
1461
         *
1462
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
1463
         * @return self<int|string,mixed> New map
1464
         */
1465
        public function except( $keys ) : self
1466
        {
1467
                return ( clone $this )->remove( $keys );
8✔
1468
        }
1469

1470

1471
        /**
1472
         * Applies a filter to all elements of the map and returns a new map.
1473
         *
1474
         * Examples:
1475
         *  Map::from( [null, 0, 1, '', '0', 'a'] )->filter();
1476
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->filter( function( $value, $key ) {
1477
         *      return $key < 10 && $value < 'n';
1478
         *  } );
1479
         *
1480
         * Results:
1481
         *  [1, 'a']
1482
         *  ['a', 'b']
1483
         *
1484
         * If no callback is passed, all values which are empty, null or false will be
1485
         * removed if their value converted to boolean is FALSE:
1486
         *  (bool) $value === false
1487
         *
1488
         * The keys in the result map are preserved.
1489
         *
1490
         * @param  callable|null $callback Function with (item, key) parameters and returns TRUE/FALSE
1491
         * @return self<int|string,mixed> New map
1492
         */
1493
        public function filter( ?callable $callback = null ) : self
1494
        {
1495
                if( $callback ) {
80✔
1496
                        return new static( array_filter( $this->list(), $callback, ARRAY_FILTER_USE_BOTH ) );
72✔
1497
                }
1498

1499
                return new static( array_filter( $this->list() ) );
8✔
1500
        }
1501

1502

1503
        /**
1504
         * Returns the first/last matching element where the callback returns TRUE.
1505
         *
1506
         * Examples:
1507
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1508
         *      return $value >= 'b';
1509
         *  } );
1510
         *  Map::from( ['a', 'c', 'e'] )->find( function( $value, $key ) {
1511
         *      return $value >= 'b';
1512
         *  }, null, true );
1513
         *  Map::from( [] )->find( function( $value, $key ) {
1514
         *      return $value >= 'b';
1515
         *  }, 'none' );
1516
         *  Map::from( [] )->find( function( $value, $key ) {
1517
         *      return $value >= 'b';
1518
         *  }, new \Exception( 'error' ) );
1519
         *
1520
         * Results:
1521
         * The first example will return 'c' while the second will return 'e' (last element).
1522
         * The third one will return "none" and the last one will throw the exception.
1523
         *
1524
         * @param \Closure $callback Function with (value, key) parameters and returns TRUE/FALSE
1525
         * @param mixed $default Default value or exception if the map contains no elements
1526
         * @param bool $reverse TRUE to test elements from back to front, FALSE for front to back (default)
1527
         * @return mixed First matching value, passed default value or an exception
1528
         */
1529
        public function find( \Closure $callback, $default = null, bool $reverse = false )
1530
        {
1531
                foreach( ( $reverse ? array_reverse( $this->list() ) : $this->list() ) as $key => $value )
32✔
1532
                {
1533
                        if( $callback( $value, $key ) ) {
32✔
1534
                                return $value;
18✔
1535
                        }
1536
                }
1537

1538
                if( $default instanceof \Throwable ) {
16✔
1539
                        throw $default;
8✔
1540
                }
1541

1542
                return $default;
8✔
1543
        }
1544

1545

1546
        /**
1547
         * Returns the first element from the map.
1548
         *
1549
         * Examples:
1550
         *  Map::from( ['a', 'b'] )->first();
1551
         *  Map::from( [] )->first( 'x' );
1552
         *  Map::from( [] )->first( new \Exception( 'error' ) );
1553
         *  Map::from( [] )->first( function() { return rand(); } );
1554
         *
1555
         * Results:
1556
         * The first example will return 'b' and the second one 'x'. The third example
1557
         * will throw the exception passed if the map contains no elements. In the
1558
         * fourth example, a random value generated by the closure function will be
1559
         * returned.
1560
         *
1561
         * @param mixed $default Default value or exception if the map contains no elements
1562
         * @return mixed First value of map, (generated) default value or an exception
1563
         */
1564
        public function first( $default = null )
1565
        {
1566
                if( ( $value = reset( $this->list() ) ) !== false ) {
64✔
1567
                        return $value;
40✔
1568
                }
1569

1570
                if( $default instanceof \Closure ) {
32✔
1571
                        return $default();
8✔
1572
                }
1573

1574
                if( $default instanceof \Throwable ) {
24✔
1575
                        throw $default;
8✔
1576
                }
1577

1578
                return $default;
16✔
1579
        }
1580

1581

1582
        /**
1583
         * Returns the first key from the map.
1584
         *
1585
         * Examples:
1586
         *  Map::from( ['a' => 1, 'b' => 2] )->firstKey();
1587
         *  Map::from( [] )->firstKey();
1588
         *
1589
         * Results:
1590
         * The first example will return 'a' and the second one NULL.
1591
         *
1592
         * @return mixed First key of map or NULL if empty
1593
         */
1594
        public function firstKey()
1595
        {
1596
                $list = $this->list();
16✔
1597

1598
                if( function_exists( 'array_key_first' ) ) {
16✔
1599
                        return array_key_first( $list );
16✔
1600
                }
1601

1602
                reset( $list );
×
1603
                return key( $list );
×
1604
        }
1605

1606

1607
        /**
1608
         * Creates a new map with all sub-array elements added recursively withput overwriting existing keys.
1609
         *
1610
         * Examples:
1611
         *  Map::from( [[0, 1], [2, 3]] )->flat();
1612
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat();
1613
         *  Map::from( [[0, 1], [[2, 3], 4]] )->flat( 1 );
1614
         *  Map::from( [[0, 1], Map::from( [[2, 3], 4] )] )->flat();
1615
         *
1616
         * Results:
1617
         *  [0, 1, 2, 3]
1618
         *  [0, 1, 2, 3, 4]
1619
         *  [0, 1, [2, 3], 4]
1620
         *  [0, 1, 2, 3, 4]
1621
         *
1622
         * The keys are not preserved and the new map elements will be numbered from
1623
         * 0-n. A value smaller than 1 for depth will return the same map elements
1624
         * indexed from 0-n. Flattening does also work if elements implement the
1625
         * "Traversable" interface (which the Map object does).
1626
         *
1627
         * This method is similar than collapse() but doesn't replace existing elements.
1628
         * Keys are NOT preserved using this method!
1629
         *
1630
         * @param int|null $depth Number of levels to flatten multi-dimensional arrays or NULL for all
1631
         * @return self<int|string,mixed> New map with all sub-array elements added into it recursively, up to the specified depth
1632
         * @throws \InvalidArgumentException If depth must be greater or equal than 0 or NULL
1633
         */
1634
        public function flat( ?int $depth = null ) : self
1635
        {
1636
                if( $depth < 0 ) {
48✔
1637
                        throw new \InvalidArgumentException( 'Depth must be greater or equal than 0 or NULL' );
8✔
1638
                }
1639

1640
                $result = [];
40✔
1641
                $this->flatten( $this->list(), $result, $depth ?? 0x7fffffff );
40✔
1642
                return new static( $result );
40✔
1643
        }
1644

1645

1646
        /**
1647
         * Exchanges the keys with their values and vice versa.
1648
         *
1649
         * Examples:
1650
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->flip();
1651
         *
1652
         * Results:
1653
         *  ['X' => 'a', 'Y' => 'b']
1654
         *
1655
         * @return self<int|string,mixed> New map with keys as values and values as keys
1656
         */
1657
        public function flip() : self
1658
        {
1659
                return new static( array_flip( $this->list() ) );
8✔
1660
        }
1661

1662

1663
        /**
1664
         * Returns an element by key and casts it to float if possible.
1665
         *
1666
         * Examples:
1667
         *  Map::from( ['a' => true] )->float( 'a' );
1668
         *  Map::from( ['a' => 1] )->float( 'a' );
1669
         *  Map::from( ['a' => '1.1'] )->float( 'a' );
1670
         *  Map::from( ['a' => '10'] )->float( 'a' );
1671
         *  Map::from( ['a' => ['b' => ['c' => 1.1]]] )->float( 'a/b/c' );
1672
         *  Map::from( [] )->float( 'c', function() { return 1.1; } );
1673
         *  Map::from( [] )->float( 'a', 1.1 );
1674
         *
1675
         *  Map::from( [] )->float( 'b' );
1676
         *  Map::from( ['b' => ''] )->float( 'b' );
1677
         *  Map::from( ['b' => null] )->float( 'b' );
1678
         *  Map::from( ['b' => 'abc'] )->float( 'b' );
1679
         *  Map::from( ['b' => [1]] )->float( 'b' );
1680
         *  Map::from( ['b' => #resource] )->float( 'b' );
1681
         *  Map::from( ['b' => new \stdClass] )->float( 'b' );
1682
         *
1683
         *  Map::from( [] )->float( 'c', new \Exception( 'error' ) );
1684
         *
1685
         * Results:
1686
         * The first eight examples will return the float values for the passed keys
1687
         * while the 9th to 14th example returns 0. The last example will throw an exception.
1688
         *
1689
         * This does also work for multi-dimensional arrays by passing the keys
1690
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1691
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1692
         * public properties of objects or objects implementing __isset() and __get() methods.
1693
         *
1694
         * @param int|string $key Key or path to the requested item
1695
         * @param mixed $default Default value if key isn't found (will be casted to float)
1696
         * @return float Value from map or default value
1697
         */
1698
        public function float( $key, $default = 0.0 ) : float
1699
        {
1700
                return (float) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
1701
        }
1702

1703

1704
        /**
1705
         * Returns an element from the map by key.
1706
         *
1707
         * Examples:
1708
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'a' );
1709
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->get( 'c', 'Z' );
1710
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->get( 'a/b/c' );
1711
         *  Map::from( [] )->get( 'Y', new \Exception( 'error' ) );
1712
         *  Map::from( [] )->get( 'Y', function() { return rand(); } );
1713
         *
1714
         * Results:
1715
         * The first example will return 'X', the second 'Z' and the third 'Y'. The forth
1716
         * example will throw the exception passed if the map contains no elements. In
1717
         * the fifth example, a random value generated by the closure function will be
1718
         * returned.
1719
         *
1720
         * This does also work for multi-dimensional arrays by passing the keys
1721
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1722
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1723
         * public properties of objects or objects implementing __isset() and __get() methods.
1724
         *
1725
         * @param int|string $key Key or path to the requested item
1726
         * @param mixed $default Default value if no element matches
1727
         * @return mixed Value from map or default value
1728
         */
1729
        public function get( $key, $default = null )
1730
        {
1731
                $list = $this->list();
184✔
1732

1733
                if( array_key_exists( $key, $list ) ) {
184✔
1734
                        return $list[$key];
48✔
1735
                }
1736

1737
                if( ( $v = $this->val( $list, explode( $this->sep, (string) $key ) ) ) !== null ) {
168✔
1738
                        return $v;
56✔
1739
                }
1740

1741
                if( $default instanceof \Closure ) {
144✔
1742
                        return $default();
48✔
1743
                }
1744

1745
                if( $default instanceof \Throwable ) {
96✔
1746
                        throw $default;
48✔
1747
                }
1748

1749
                return $default;
48✔
1750
        }
1751

1752

1753
        /**
1754
         * Returns an iterator for the elements.
1755
         *
1756
         * This method will be used by e.g. foreach() to loop over all entries:
1757
         *  foreach( Map::from( ['a', 'b'] ) as $value )
1758
         *
1759
         * @return \ArrayIterator<int|string,mixed> Iterator for map elements
1760
         */
1761
        public function getIterator() : \ArrayIterator
1762
        {
1763
                return new \ArrayIterator( $this->list = $this->array( $this->list ) );
40✔
1764
        }
1765

1766

1767
        /**
1768
         * Returns only items which matches the regular expression.
1769
         *
1770
         * All items are converted to string first before they are compared to the
1771
         * regular expression. Thus, fractions of ".0" will be removed in float numbers
1772
         * which may result in unexpected results.
1773
         *
1774
         * Examples:
1775
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/b/' );
1776
         *  Map::from( ['ab', 'bc', 'cd'] )->grep( '/a/', PREG_GREP_INVERT );
1777
         *  Map::from( [1.5, 0, 1.0, 'a'] )->grep( '/^(\d+)?\.\d+$/' );
1778
         *
1779
         * Results:
1780
         *  ['ab', 'bc']
1781
         *  ['bc', 'cd']
1782
         *  [1.5] // float 1.0 is converted to string "1"
1783
         *
1784
         * The keys are preserved using this method.
1785
         *
1786
         * @param string $pattern Regular expression pattern, e.g. "/ab/"
1787
         * @param int $flags PREG_GREP_INVERT to return elements not matching the pattern
1788
         * @return self<int|string,mixed> New map containing only the matched elements
1789
         */
1790
        public function grep( string $pattern, int $flags = 0 ) : self
1791
        {
1792
                if( ( $result = preg_grep( $pattern, $this->list(), $flags ) ) === false )
32✔
1793
                {
1794
                        switch( preg_last_error() )
8✔
1795
                        {
1796
                                case PREG_INTERNAL_ERROR: $msg = 'Internal error'; break;
8✔
1797
                                case PREG_BACKTRACK_LIMIT_ERROR: $msg = 'Backtrack limit error'; break;
×
1798
                                case PREG_RECURSION_LIMIT_ERROR: $msg = 'Recursion limit error'; break;
×
1799
                                case PREG_BAD_UTF8_ERROR: $msg = 'Bad UTF8 error'; break;
×
1800
                                case PREG_BAD_UTF8_OFFSET_ERROR: $msg = 'Bad UTF8 offset error'; break;
×
1801
                                case PREG_JIT_STACKLIMIT_ERROR: $msg = 'JIT stack limit error'; break;
×
1802
                                default: $msg = 'Unknown error';
×
1803
                        }
1804

1805
                        throw new \RuntimeException( 'Regular expression error: ' . $msg );
8✔
1806
                }
1807

1808
                return new static( $result );
24✔
1809
        }
1810

1811

1812
        /**
1813
         * Groups associative array elements or objects by the passed key or closure.
1814
         *
1815
         * Instead of overwriting items with the same keys like to the col() method
1816
         * does, groupBy() keeps all entries in sub-arrays. It's preserves the keys
1817
         * of the orignal map entries too.
1818
         *
1819
         * Examples:
1820
         *  $list = [
1821
         *    10 => ['aid' => 123, 'code' => 'x-abc'],
1822
         *    20 => ['aid' => 123, 'code' => 'x-def'],
1823
         *    30 => ['aid' => 456, 'code' => 'x-def']
1824
         *  ];
1825
         *  Map::from( $list )->groupBy( 'aid' );
1826
         *  Map::from( $list )->groupBy( function( $item, $key ) {
1827
         *    return substr( $item['code'], -3 );
1828
         *  } );
1829
         *  Map::from( $list )->groupBy( 'xid' );
1830
         *
1831
         * Results:
1832
         *  [
1833
         *    123 => [10 => ['aid' => 123, 'code' => 'x-abc'], 20 => ['aid' => 123, 'code' => 'x-def']],
1834
         *    456 => [30 => ['aid' => 456, 'code' => 'x-def']]
1835
         *  ]
1836
         *  [
1837
         *    'abc' => [10 => ['aid' => 123, 'code' => 'x-abc']],
1838
         *    'def' => [20 => ['aid' => 123, 'code' => 'x-def'], 30 => ['aid' => 456, 'code' => 'x-def']]
1839
         *  ]
1840
         *  [
1841
         *    '' => [
1842
         *      10 => ['aid' => 123, 'code' => 'x-abc'],
1843
         *      20 => ['aid' => 123, 'code' => 'x-def'],
1844
         *      30 => ['aid' => 456, 'code' => 'x-def']
1845
         *    ]
1846
         *  ]
1847
         *
1848
         * In case the passed key doesn't exist in one or more items, these items
1849
         * are stored in a sub-array using an empty string as key.
1850
         *
1851
         * @param  \Closure|string|int $key Closure function with (item, idx) parameters returning the key or the key itself to group by
1852
         * @return self<int|string,mixed> New map with elements grouped by the given key
1853
         */
1854
        public function groupBy( $key ) : self
1855
        {
1856
                $result = [];
32✔
1857

1858
                foreach( $this->list() as $idx => $item )
32✔
1859
                {
1860
                        if( is_callable( $key ) ) {
32✔
1861
                                $keyval = $key( $item, $idx );
8✔
1862
                        } elseif( ( is_array( $item ) || $item instanceof \ArrayAccess ) && isset( $item[$key] ) ) {
24✔
1863
                                $keyval = $item[$key];
8✔
1864
                        } elseif( is_object( $item ) && isset( $item->{$key} ) ) {
16✔
1865
                                $keyval = $item->{$key};
8✔
1866
                        } else {
1867
                                $keyval = '';
8✔
1868
                        }
1869

1870
                        $result[$keyval][$idx] = $item;
32✔
1871
                }
1872

1873
                return new static( $result );
32✔
1874
        }
1875

1876

1877
        /**
1878
         * Determines if a key or several keys exists in the map.
1879
         *
1880
         * If several keys are passed as array, all keys must exist in the map for
1881
         * TRUE to be returned.
1882
         *
1883
         * Examples:
1884
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'a' );
1885
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'b'] );
1886
         *  Map::from( ['a' => ['b' => ['c' => 'Y']]] )->has( 'a/b/c' );
1887
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'c' );
1888
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( ['a', 'c'] );
1889
         *  Map::from( ['a' => 'X', 'b' => 'Y'] )->has( 'X' );
1890
         *
1891
         * Results:
1892
         * The first three examples will return TRUE while the other ones will return FALSE
1893
         *
1894
         * This does also work for multi-dimensional arrays by passing the keys
1895
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
1896
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
1897
         * public properties of objects or objects implementing __isset() and __get() methods.
1898
         *
1899
         * @param array<int|string>|int|string $key Key of the requested item or list of keys
1900
         * @return bool TRUE if key or keys are available in map, FALSE if not
1901
         */
1902
        public function has( $key ) : bool
1903
        {
1904
                $list = $this->list();
24✔
1905

1906
                foreach( (array) $key as $entry )
24✔
1907
                {
1908
                        if( array_key_exists( $entry, $list ) === false
24✔
1909
                                && $this->val( $list, explode( $this->sep, (string) $entry ) ) === null
24✔
1910
                        ) {
1911
                                return false;
24✔
1912
                        }
1913
                }
1914

1915
                return true;
24✔
1916
        }
1917

1918

1919
        /**
1920
         * Executes callbacks depending on the condition.
1921
         *
1922
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
1923
         * executed and their returned value is passed back within a Map object. In
1924
         * case no "then" or "else" closure is given, the method will return the same
1925
         * map object.
1926
         *
1927
         * Examples:
1928
         *  Map::from( [] )->if( strpos( 'abc', 'b' ) !== false, function( $map ) {
1929
         *    echo 'found';
1930
         *  } );
1931
         *
1932
         *  Map::from( [] )->if( function( $map ) {
1933
         *    return $map->empty();
1934
         *  }, function( $map ) {
1935
         *    echo 'then';
1936
         *  } );
1937
         *
1938
         *  Map::from( ['a'] )->if( function( $map ) {
1939
         *    return $map->empty();
1940
         *  }, function( $map ) {
1941
         *    echo 'then';
1942
         *  }, function( $map ) {
1943
         *    echo 'else';
1944
         *  } );
1945
         *
1946
         *  Map::from( ['a', 'b'] )->if( true, function( $map ) {
1947
         *    return $map->push( 'c' );
1948
         *  } );
1949
         *
1950
         *  Map::from( ['a', 'b'] )->if( false, null, function( $map ) {
1951
         *    return $map->pop();
1952
         *  } );
1953
         *
1954
         * Results:
1955
         * The first example returns "found" while the second one returns "then" and
1956
         * the third one "else". The forth one will return ['a', 'b', 'c'] while the
1957
         * fifth one will return 'b', which is turned into a map of ['b'] again.
1958
         *
1959
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
1960
         * (a short form for anonymous closures) as parameters. The automatically have access
1961
         * to previously defined variables but can not modify them. Also, they can not have
1962
         * a void return type and must/will always return something. Details about
1963
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
1964
         *
1965
         * @param \Closure|bool $condition Boolean or function with (map) parameter returning a boolean
1966
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
1967
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
1968
         * @return self<int|string,mixed> New map
1969
         */
1970
        public function if( $condition, ?\Closure $then = null, ?\Closure $else = null ) : self
1971
        {
1972
                if( $condition instanceof \Closure ) {
64✔
1973
                        $condition = $condition( $this );
16✔
1974
                }
1975

1976
                if( $condition ) {
64✔
1977
                        return $then ? static::from( $then( $this, $condition ) ) : $this;
40✔
1978
                } elseif( $else ) {
24✔
1979
                        return static::from( $else( $this, $condition ) );
24✔
1980
                }
1981

1982
                return $this;
×
1983
        }
1984

1985

1986
        /**
1987
         * Executes callbacks depending if the map contains elements or not.
1988
         *
1989
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
1990
         * executed and their returned value is passed back within a Map object. In
1991
         * case no "then" or "else" closure is given, the method will return the same
1992
         * map object.
1993
         *
1994
         * Examples:
1995
         *  Map::from( ['a'] )->ifAny( function( $map ) {
1996
         *    $map->push( 'b' );
1997
         *  } );
1998
         *
1999
         *  Map::from( [] )->ifAny( null, function( $map ) {
2000
         *    return $map->push( 'b' );
2001
         *  } );
2002
         *
2003
         *  Map::from( ['a'] )->ifAny( function( $map ) {
2004
         *    return 'c';
2005
         *  } );
2006
         *
2007
         * Results:
2008
         * The first example returns a Map containing ['a', 'b'] because the the initial
2009
         * Map is not empty. The second one returns  a Map with ['b'] because the initial
2010
         * Map is empty and the "else" closure is used. The last example returns ['c']
2011
         * as new map content.
2012
         *
2013
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2014
         * (a short form for anonymous closures) as parameters. The automatically have access
2015
         * to previously defined variables but can not modify them. Also, they can not have
2016
         * a void return type and must/will always return something. Details about
2017
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2018
         *
2019
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2020
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2021
         * @return self<int|string,mixed> New map
2022
         */
2023
        public function ifAny( ?\Closure $then = null, ?\Closure $else = null ) : self
2024
        {
2025
                return $this->if( !empty( $this->list() ), $then, $else );
24✔
2026
        }
2027

2028

2029
        /**
2030
         * Executes callbacks depending if the map is empty or not.
2031
         *
2032
         * If callbacks for "then" and/or "else" are passed, these callbacks will be
2033
         * executed and their returned value is passed back within a Map object. In
2034
         * case no "then" or "else" closure is given, the method will return the same
2035
         * map object.
2036
         *
2037
         * Examples:
2038
         *  Map::from( [] )->ifEmpty( function( $map ) {
2039
         *    $map->push( 'a' );
2040
         *  } );
2041
         *
2042
         *  Map::from( ['a'] )->ifEmpty( null, function( $map ) {
2043
         *    return $map->push( 'b' );
2044
         *  } );
2045
         *
2046
         * Results:
2047
         * The first example returns a Map containing ['a'] because the the initial Map
2048
         * is empty. The second one returns  a Map with ['a', 'b'] because the initial
2049
         * Map is not empty and the "else" closure is used.
2050
         *
2051
         * Since PHP 7.4, you can also pass arrow function like `fn($map) => $map->has('c')`
2052
         * (a short form for anonymous closures) as parameters. The automatically have access
2053
         * to previously defined variables but can not modify them. Also, they can not have
2054
         * a void return type and must/will always return something. Details about
2055
         * [PHP arrow functions](https://www.php.net/manual/en/functions.arrow.php)
2056
         *
2057
         * @param \Closure|null $then Function with (map, condition) parameter (optional)
2058
         * @param \Closure|null $else Function with (map, condition) parameter (optional)
2059
         * @return self<int|string,mixed> New map
2060
         */
2061
        public function ifEmpty( ?\Closure $then = null, ?\Closure $else = null ) : self
2062
        {
2063
                return $this->if( empty( $this->list() ), $then, $else );
×
2064
        }
2065

2066

2067
        /**
2068
         * Tests if all entries in the map are objects implementing the given interface.
2069
         *
2070
         * Examples:
2071
         *  Map::from( [new Map(), new Map()] )->implements( '\Countable' );
2072
         *  Map::from( [new Map(), new stdClass()] )->implements( '\Countable' );
2073
         *  Map::from( [new Map(), 123] )->implements( '\Countable' );
2074
         *  Map::from( [new Map(), 123] )->implements( '\Countable', true );
2075
         *  Map::from( [new Map(), 123] )->implements( '\Countable', '\RuntimeException' );
2076
         *
2077
         * Results:
2078
         *  The first example returns TRUE while the second and third one return FALSE.
2079
         *  The forth example will throw an UnexpectedValueException while the last one
2080
         *  throws a RuntimeException.
2081
         *
2082
         * @param string $interface Name of the interface that must be implemented
2083
         * @param \Throwable|bool $throw Passing TRUE or an exception name will throw the exception instead of returning FALSE
2084
         * @return bool TRUE if all entries implement the interface or FALSE if at least one doesn't
2085
         * @throws \UnexpectedValueException|\Throwable If one entry doesn't implement the interface
2086
         */
2087
        public function implements( string $interface, $throw = false ) : bool
2088
        {
2089
                foreach( $this->list() as $entry )
24✔
2090
                {
2091
                        if( !( $entry instanceof $interface ) )
24✔
2092
                        {
2093
                                if( $throw )
24✔
2094
                                {
2095
                                        $name = is_string( $throw ) ? $throw : '\UnexpectedValueException';
16✔
2096
                                        throw new $name( "Does not implement $interface: " . print_r( $entry, true ) );
16✔
2097
                                }
2098

2099
                                return false;
10✔
2100
                        }
2101
                }
2102

2103
                return true;
8✔
2104
        }
2105

2106

2107
        /**
2108
         * Tests if the passed element or elements are part of the map.
2109
         *
2110
         * Examples:
2111
         *  Map::from( ['a', 'b'] )->in( 'a' );
2112
         *  Map::from( ['a', 'b'] )->in( ['a', 'b'] );
2113
         *  Map::from( ['a', 'b'] )->in( 'x' );
2114
         *  Map::from( ['a', 'b'] )->in( ['a', 'x'] );
2115
         *  Map::from( ['1', '2'] )->in( 2, true );
2116
         *
2117
         * Results:
2118
         * The first and second example will return TRUE while the other ones will return FALSE
2119
         *
2120
         * @param mixed|array $element Element or elements to search for in the map
2121
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2122
         * @return bool TRUE if all elements are available in map, FALSE if not
2123
         */
2124
        public function in( $element, bool $strict = false ) : bool
2125
        {
2126
                if( !is_array( $element ) ) {
32✔
2127
                        return in_array( $element, $this->list(), $strict );
32✔
2128
                };
2129

2130
                foreach( $element as $entry )
8✔
2131
                {
2132
                        if( in_array( $entry, $this->list(), $strict ) === false ) {
8✔
2133
                                return false;
8✔
2134
                        }
2135
                }
2136

2137
                return true;
8✔
2138
        }
2139

2140

2141
        /**
2142
         * Tests if the passed element or elements are part of the map.
2143
         *
2144
         * This method is an alias for in(). For performance reasons, in() should be
2145
         * preferred because it uses one method call less than includes().
2146
         *
2147
         * @param mixed|array $element Element or elements to search for in the map
2148
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
2149
         * @return bool TRUE if all elements are available in map, FALSE if not
2150
         * @see in() - Underlying method with same parameters and return value but better performance
2151
         */
2152
        public function includes( $element, bool $strict = false ) : bool
2153
        {
2154
                return $this->in( $element, $strict );
8✔
2155
        }
2156

2157

2158
        /**
2159
         * Returns the numerical index of the given key.
2160
         *
2161
         * Examples:
2162
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( '8' );
2163
         *  Map::from( [4 => 'a', 8 => 'b'] )->index( function( $key ) {
2164
         *      return $key == '8';
2165
         *  } );
2166
         *
2167
         * Results:
2168
         * Both examples will return "1" because the value "b" is at the second position
2169
         * and the returned index is zero based so the first item has the index "0".
2170
         *
2171
         * @param \Closure|string|int $value Key to search for or function with (key) parameters return TRUE if key is found
2172
         * @return int|null Position of the found value (zero based) or NULL if not found
2173
         */
2174
        public function index( $value ) : ?int
2175
        {
2176
                if( $value instanceof \Closure )
32✔
2177
                {
2178
                        $pos = 0;
16✔
2179

2180
                        foreach( $this->list() as $key => $item )
16✔
2181
                        {
2182
                                if( $value( $key ) ) {
8✔
2183
                                        return $pos;
8✔
2184
                                }
2185

2186
                                ++$pos;
8✔
2187
                        }
2188

2189
                        return null;
8✔
2190
                }
2191

2192
                $pos = array_search( $value, array_keys( $this->list() ) );
16✔
2193
                return $pos !== false ? $pos : null;
16✔
2194
        }
2195

2196

2197
        /**
2198
         * Inserts the value or values after the given element.
2199
         *
2200
         * Examples:
2201
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAfter( 'foo', 'baz' );
2202
         *  Map::from( ['foo', 'bar'] )->insertAfter( 'foo', ['baz', 'boo'] );
2203
         *  Map::from( ['foo', 'bar'] )->insertAfter( null, 'baz' );
2204
         *
2205
         * Results:
2206
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2207
         *  ['foo', 'baz', 'boo', 'bar']
2208
         *  ['foo', 'bar', 'baz']
2209
         *
2210
         * Numerical array indexes are not preserved.
2211
         *
2212
         * @param mixed $element Element after the value is inserted
2213
         * @param mixed $value Element or list of elements to insert
2214
         * @return self<int|string,mixed> Updated map for fluid interface
2215
         */
2216
        public function insertAfter( $element, $value ) : self
2217
        {
2218
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
24✔
2219
                array_splice( $this->list(), $position + 1, 0, $this->array( $value ) );
24✔
2220

2221
                return $this;
24✔
2222
        }
2223

2224

2225
        /**
2226
         * Inserts the item at the given position in the map.
2227
         *
2228
         * Examples:
2229
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 0, 'baz' );
2230
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 1, 'baz', 'c' );
2231
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( 4, 'baz' );
2232
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertAt( -1, 'baz', 'c' );
2233
         *
2234
         * Results:
2235
         *  [0 => 'baz', 'a' => 'foo', 'b' => 'bar']
2236
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2237
         *  ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']
2238
         *  ['a' => 'foo', 'c' => 'baz', 'b' => 'bar']
2239
         *
2240
         * @param int $pos Position the element it should be inserted at
2241
         * @param mixed $element Element to be inserted
2242
         * @param mixed|null $key Element key or NULL to assign an integer key automatically
2243
         * @return self<int|string,mixed> Updated map for fluid interface
2244
         */
2245
        public function insertAt( int $pos, $element, $key = null ) : self
2246
        {
2247
                if( $key !== null )
40✔
2248
                {
2249
                        $list = $this->list();
16✔
2250

2251
                        $this->list = array_merge(
16✔
2252
                                array_slice( $list, 0, $pos, true ),
16✔
2253
                                [$key => $element],
16✔
2254
                                array_slice( $list, $pos, null, true )
16✔
2255
                        );
12✔
2256
                }
2257
                else
2258
                {
2259
                        array_splice( $this->list(), $pos, 0, [$element] );
24✔
2260
                }
2261

2262
                return $this;
40✔
2263
        }
2264

2265

2266
        /**
2267
         * Inserts the value or values before the given element.
2268
         *
2269
         * Examples:
2270
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->insertBefore( 'bar', 'baz' );
2271
         *  Map::from( ['foo', 'bar'] )->insertBefore( 'bar', ['baz', 'boo'] );
2272
         *  Map::from( ['foo', 'bar'] )->insertBefore( null, 'baz' );
2273
         *
2274
         * Results:
2275
         *  ['a' => 'foo', 0 => 'baz', 'b' => 'bar']
2276
         *  ['foo', 'baz', 'boo', 'bar']
2277
         *  ['foo', 'bar', 'baz']
2278
         *
2279
         * Numerical array indexes are not preserved.
2280
         *
2281
         * @param mixed $element Element before the value is inserted
2282
         * @param mixed $value Element or list of elements to insert
2283
         * @return self<int|string,mixed> Updated map for fluid interface
2284
         */
2285
        public function insertBefore( $element, $value ) : self
2286
        {
2287
                $position = ( $element !== null && ( $pos = $this->pos( $element ) ) !== null ? $pos : count( $this->list() ) );
24✔
2288
                array_splice( $this->list(), $position, 0, $this->array( $value ) );
24✔
2289

2290
                return $this;
24✔
2291
        }
2292

2293

2294
        /**
2295
         * Tests if the passed value or values are part of the strings in the map.
2296
         *
2297
         * Examples:
2298
         *  Map::from( ['abc'] )->inString( 'c' );
2299
         *  Map::from( ['abc'] )->inString( 'bc' );
2300
         *  Map::from( [12345] )->inString( '23' );
2301
         *  Map::from( [123.4] )->inString( 23.4 );
2302
         *  Map::from( [12345] )->inString( false );
2303
         *  Map::from( [12345] )->inString( true );
2304
         *  Map::from( [false] )->inString( false );
2305
         *  Map::from( ['abc'] )->inString( '' );
2306
         *  Map::from( [''] )->inString( false );
2307
         *  Map::from( ['abc'] )->inString( 'BC', false );
2308
         *  Map::from( ['abc', 'def'] )->inString( ['de', 'xy'] );
2309
         *  Map::from( ['abc', 'def'] )->inString( ['E', 'x'] );
2310
         *  Map::from( ['abc', 'def'] )->inString( 'E' );
2311
         *  Map::from( [23456] )->inString( true );
2312
         *  Map::from( [false] )->inString( 0 );
2313
         *
2314
         * Results:
2315
         * The first eleven examples will return TRUE while the last four will return FALSE
2316
         *
2317
         * All scalar values (bool, float, int and string) are casted to string values before
2318
         * comparing to the given value. Non-scalar values in the map are ignored.
2319
         *
2320
         * @param array|string $value Value or values to compare the map elements, will be casted to string type
2321
         * @param bool $case TRUE if comparison is case sensitive, FALSE to ignore upper/lower case
2322
         * @return bool TRUE If at least one element matches, FALSE if value is not in any string of the map
2323
         * @deprecated Use multi-byte aware strContains() instead
2324
         */
2325
        public function inString( $value, bool $case = true ) : bool
2326
        {
2327
                $fcn = $case ? 'strpos' : 'stripos';
8✔
2328

2329
                foreach( (array) $value as $val )
8✔
2330
                {
2331
                        if( (string) $val === '' ) {
8✔
2332
                                return true;
8✔
2333
                        }
2334

2335
                        foreach( $this->list() as $item )
8✔
2336
                        {
2337
                                if( is_scalar( $item ) && $fcn( (string) $item, (string) $val ) !== false ) {
8✔
2338
                                        return true;
8✔
2339
                                }
2340
                        }
2341
                }
2342

2343
                return false;
8✔
2344
        }
2345

2346

2347
        /**
2348
         * Returns an element by key and casts it to integer if possible.
2349
         *
2350
         * Examples:
2351
         *  Map::from( ['a' => true] )->int( 'a' );
2352
         *  Map::from( ['a' => '1'] )->int( 'a' );
2353
         *  Map::from( ['a' => 1.1] )->int( 'a' );
2354
         *  Map::from( ['a' => '10'] )->int( 'a' );
2355
         *  Map::from( ['a' => ['b' => ['c' => 1]]] )->int( 'a/b/c' );
2356
         *  Map::from( [] )->int( 'c', function() { return rand( 1, 1 ); } );
2357
         *  Map::from( [] )->int( 'a', 1 );
2358
         *
2359
         *  Map::from( [] )->int( 'b' );
2360
         *  Map::from( ['b' => ''] )->int( 'b' );
2361
         *  Map::from( ['b' => 'abc'] )->int( 'b' );
2362
         *  Map::from( ['b' => null] )->int( 'b' );
2363
         *  Map::from( ['b' => [1]] )->int( 'b' );
2364
         *  Map::from( ['b' => #resource] )->int( 'b' );
2365
         *  Map::from( ['b' => new \stdClass] )->int( 'b' );
2366
         *
2367
         *  Map::from( [] )->int( 'c', new \Exception( 'error' ) );
2368
         *
2369
         * Results:
2370
         * The first seven examples will return 1 while the 8th to 14th example
2371
         * returns 0. The last example will throw an exception.
2372
         *
2373
         * This does also work for multi-dimensional arrays by passing the keys
2374
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2375
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2376
         * public properties of objects or objects implementing __isset() and __get() methods.
2377
         *
2378
         * @param int|string $key Key or path to the requested item
2379
         * @param mixed $default Default value if key isn't found (will be casted to integer)
2380
         * @return int Value from map or default value
2381
         */
2382
        public function int( $key, $default = 0 ) : int
2383
        {
2384
                return (int) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
2385
        }
2386

2387

2388
        /**
2389
         * Returns all values in a new map that are available in both, the map and the given elements.
2390
         *
2391
         * Examples:
2392
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersect( ['bar'] );
2393
         *
2394
         * Results:
2395
         *  ['b' => 'bar']
2396
         *
2397
         * If a callback is passed, the given function will be used to compare the values.
2398
         * The function must accept two parameters (value A and B) and must return
2399
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2400
         * greater than value B. Both, a method name and an anonymous function can be passed:
2401
         *
2402
         *  Map::from( [0 => 'a'] )->intersect( [0 => 'A'], 'strcasecmp' );
2403
         *  Map::from( ['b' => 'a'] )->intersect( ['B' => 'A'], 'strcasecmp' );
2404
         *  Map::from( ['b' => 'a'] )->intersect( ['c' => 'A'], function( $valA, $valB ) {
2405
         *      return strtolower( $valA ) <=> strtolower( $valB );
2406
         *  } );
2407
         *
2408
         * All examples will return a map containing ['a'] because both contain the same
2409
         * values when compared case insensitive.
2410
         *
2411
         * The keys are preserved using this method.
2412
         *
2413
         * @param iterable<int|string,mixed> $elements List of elements
2414
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2415
         * @return self<int|string,mixed> New map
2416
         */
2417
        public function intersect( iterable $elements, ?callable $callback = null ) : self
2418
        {
2419
                $list = $this->list();
16✔
2420
                $elements = $this->array( $elements );
16✔
2421

2422
                if( $callback ) {
16✔
2423
                        return new static( array_uintersect( $list, $elements, $callback ) );
8✔
2424
                }
2425

2426
                // using array_intersect() is 7x slower
2427
                return ( new static( $list ) )
8✔
2428
                        ->remove( array_keys( array_diff( $list, $elements ) ) )
8✔
2429
                        ->remove( array_keys( array_diff( $elements, $list ) ) );
8✔
2430
        }
2431

2432

2433
        /**
2434
         * Returns all values in a new map that are available in both, the map and the given elements while comparing the keys too.
2435
         *
2436
         * Examples:
2437
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectAssoc( new Map( ['foo', 'b' => 'bar'] ) );
2438
         *
2439
         * Results:
2440
         *  ['a' => 'foo']
2441
         *
2442
         * If a callback is passed, the given function will be used to compare the values.
2443
         * The function must accept two parameters (value A and B) and must return
2444
         * -1 if value A is smaller than value B, 0 if both are equal and 1 if value A is
2445
         * greater than value B. Both, a method name and an anonymous function can be passed:
2446
         *
2447
         *  Map::from( [0 => 'a'] )->intersectAssoc( [0 => 'A'], 'strcasecmp' );
2448
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['B' => 'A'], 'strcasecmp' );
2449
         *  Map::from( ['b' => 'a'] )->intersectAssoc( ['c' => 'A'], function( $valA, $valB ) {
2450
         *      return strtolower( $valA ) <=> strtolower( $valB );
2451
         *  } );
2452
         *
2453
         * The first example will return [0 => 'a'] because both contain the same
2454
         * values when compared case insensitive. The second and third example will return
2455
         * an empty map because the keys doesn't match ("b" vs. "B" and "b" vs. "c").
2456
         *
2457
         * The keys are preserved using this method.
2458
         *
2459
         * @param iterable<int|string,mixed> $elements List of elements
2460
         * @param  callable|null $callback Function with (valueA, valueB) parameters and returns -1 (<), 0 (=) and 1 (>)
2461
         * @return self<int|string,mixed> New map
2462
         */
2463
        public function intersectAssoc( iterable $elements, ?callable $callback = null ) : self
2464
        {
2465
                $elements = $this->array( $elements );
40✔
2466

2467
                if( $callback ) {
40✔
2468
                        return new static( array_uintersect_assoc( $this->list(), $elements, $callback ) );
8✔
2469
                }
2470

2471
                return new static( array_intersect_assoc( $this->list(), $elements ) );
32✔
2472
        }
2473

2474

2475
        /**
2476
         * Returns all values in a new map that are available in both, the map and the given elements by comparing the keys only.
2477
         *
2478
         * Examples:
2479
         *  Map::from( ['a' => 'foo', 'b' => 'bar'] )->intersectKeys( new Map( ['foo', 'b' => 'baz'] ) );
2480
         *
2481
         * Results:
2482
         *  ['b' => 'bar']
2483
         *
2484
         * If a callback is passed, the given function will be used to compare the keys.
2485
         * The function must accept two parameters (key A and B) and must return
2486
         * -1 if key A is smaller than key B, 0 if both are equal and 1 if key A is
2487
         * greater than key B. Both, a method name and an anonymous function can be passed:
2488
         *
2489
         *  Map::from( [0 => 'a'] )->intersectKeys( [0 => 'A'], 'strcasecmp' );
2490
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['B' => 'X'], 'strcasecmp' );
2491
         *  Map::from( ['b' => 'a'] )->intersectKeys( ['c' => 'a'], function( $keyA, $keyB ) {
2492
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
2493
         *  } );
2494
         *
2495
         * The first example will return a map with [0 => 'a'] and the second one will
2496
         * return a map with ['b' => 'a'] because both contain the same keys when compared
2497
         * case insensitive. The third example will return an empty map because the keys
2498
         * doesn't match ("b" vs. "c").
2499
         *
2500
         * The keys are preserved using this method.
2501
         *
2502
         * @param iterable<int|string,mixed> $elements List of elements
2503
         * @param  callable|null $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
2504
         * @return self<int|string,mixed> New map
2505
         */
2506
        public function intersectKeys( iterable $elements, ?callable $callback = null ) : self
2507
        {
2508
                $list = $this->list();
24✔
2509
                $elements = $this->array( $elements );
24✔
2510

2511
                if( $callback ) {
24✔
2512
                        return new static( array_intersect_ukey( $list, $elements, $callback ) );
8✔
2513
                }
2514

2515
                // using array_intersect_key() is 1.6x slower
2516
                return ( new static( $list ) )
16✔
2517
                        ->remove( array_keys( array_diff_key( $list, $elements ) ) )
16✔
2518
                        ->remove( array_keys( array_diff_key( $elements, $list ) ) );
16✔
2519
        }
2520

2521

2522
        /**
2523
         * Tests if the map consists of the same keys and values
2524
         *
2525
         * Examples:
2526
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'] );
2527
         *  Map::from( ['a', 'b'] )->is( ['b', 'a'], true );
2528
         *  Map::from( [1, 2] )->is( ['1', '2'] );
2529
         *
2530
         * Results:
2531
         *  The first example returns TRUE while the second and third one returns FALSE
2532
         *
2533
         * @param iterable<int|string,mixed> $list List of key/value pairs to compare with
2534
         * @param bool $strict TRUE for comparing order of elements too, FALSE for key/values only
2535
         * @return bool TRUE if given list is equal, FALSE if not
2536
         */
2537
        public function is( iterable $list, bool $strict = false ) : bool
2538
        {
2539
                $list = $this->array( $list );
24✔
2540

2541
                if( $strict ) {
24✔
2542
                        return $this->list() === $list;
16✔
2543
                }
2544

2545
                return $this->list() == $list;
8✔
2546
        }
2547

2548

2549
        /**
2550
         * Determines if the map is empty or not.
2551
         *
2552
         * Examples:
2553
         *  Map::from( [] )->isEmpty();
2554
         *  Map::from( ['a'] )->isEmpty();
2555
         *
2556
         * Results:
2557
         *  The first example returns TRUE while the second returns FALSE
2558
         *
2559
         * The method is equivalent to empty().
2560
         *
2561
         * @return bool TRUE if map is empty, FALSE if not
2562
         */
2563
        public function isEmpty() : bool
2564
        {
2565
                return empty( $this->list() );
32✔
2566
        }
2567

2568

2569
        /**
2570
         * Determines if all entries are numeric values.
2571
         *
2572
         * Examples:
2573
         *  Map::from( [] )->isNumeric();
2574
         *  Map::from( [1] )->isNumeric();
2575
         *  Map::from( [1.1] )->isNumeric();
2576
         *  Map::from( [010] )->isNumeric();
2577
         *  Map::from( [0x10] )->isNumeric();
2578
         *  Map::from( [0b10] )->isNumeric();
2579
         *  Map::from( ['010'] )->isNumeric();
2580
         *  Map::from( ['10'] )->isNumeric();
2581
         *  Map::from( ['10.1'] )->isNumeric();
2582
         *  Map::from( [' 10 '] )->isNumeric();
2583
         *  Map::from( ['10e2'] )->isNumeric();
2584
         *  Map::from( ['0b10'] )->isNumeric();
2585
         *  Map::from( ['0x10'] )->isNumeric();
2586
         *  Map::from( ['null'] )->isNumeric();
2587
         *  Map::from( [null] )->isNumeric();
2588
         *  Map::from( [true] )->isNumeric();
2589
         *  Map::from( [[]] )->isNumeric();
2590
         *  Map::from( [''] )->isNumeric();
2591
         *
2592
         * Results:
2593
         *  The first eleven examples return TRUE while the last seven return FALSE
2594
         *
2595
         * @return bool TRUE if all map entries are numeric values, FALSE if not
2596
         */
2597
        public function isNumeric() : bool
2598
        {
2599
                foreach( $this->list() as $val )
8✔
2600
                {
2601
                        if( !is_numeric( $val ) ) {
8✔
2602
                                return false;
8✔
2603
                        }
2604
                }
2605

2606
                return true;
8✔
2607
        }
2608

2609

2610
        /**
2611
         * Determines if all entries are objects.
2612
         *
2613
         * Examples:
2614
         *  Map::from( [] )->isObject();
2615
         *  Map::from( [new stdClass] )->isObject();
2616
         *  Map::from( [1] )->isObject();
2617
         *
2618
         * Results:
2619
         *  The first two examples return TRUE while the last one return FALSE
2620
         *
2621
         * @return bool TRUE if all map entries are objects, FALSE if not
2622
         */
2623
        public function isObject() : bool
2624
        {
2625
                foreach( $this->list() as $val )
8✔
2626
                {
2627
                        if( !is_object( $val ) ) {
8✔
2628
                                return false;
8✔
2629
                        }
2630
                }
2631

2632
                return true;
8✔
2633
        }
2634

2635

2636
        /**
2637
         * Determines if all entries are scalar values.
2638
         *
2639
         * Examples:
2640
         *  Map::from( [] )->isScalar();
2641
         *  Map::from( [1] )->isScalar();
2642
         *  Map::from( [1.1] )->isScalar();
2643
         *  Map::from( ['abc'] )->isScalar();
2644
         *  Map::from( [true, false] )->isScalar();
2645
         *  Map::from( [new stdClass] )->isScalar();
2646
         *  Map::from( [#resource] )->isScalar();
2647
         *  Map::from( [null] )->isScalar();
2648
         *  Map::from( [[1]] )->isScalar();
2649
         *
2650
         * Results:
2651
         *  The first five examples return TRUE while the others return FALSE
2652
         *
2653
         * @return bool TRUE if all map entries are scalar values, FALSE if not
2654
         */
2655
        public function isScalar() : bool
2656
        {
2657
                foreach( $this->list() as $val )
8✔
2658
                {
2659
                        if( !is_scalar( $val ) ) {
8✔
2660
                                return false;
8✔
2661
                        }
2662
                }
2663

2664
                return true;
8✔
2665
        }
2666

2667

2668
        /**
2669
         * Determines if all entries are string values.
2670
         *
2671
         * Examples:
2672
         *  Map::from( ['abc'] )->isString();
2673
         *  Map::from( [] )->isString();
2674
         *  Map::from( [1] )->isString();
2675
         *  Map::from( [1.1] )->isString();
2676
         *  Map::from( [true, false] )->isString();
2677
         *  Map::from( [new stdClass] )->isString();
2678
         *  Map::from( [#resource] )->isString();
2679
         *  Map::from( [null] )->isString();
2680
         *  Map::from( [[1]] )->isString();
2681
         *
2682
         * Results:
2683
         *  The first two examples return TRUE while the others return FALSE
2684
         *
2685
         * @return bool TRUE if all map entries are string values, FALSE if not
2686
         */
2687
        public function isString() : bool
2688
        {
2689
                foreach( $this->list() as $val )
8✔
2690
                {
2691
                        if( !is_string( $val ) ) {
8✔
2692
                                return false;
8✔
2693
                        }
2694
                }
2695

2696
                return true;
8✔
2697
        }
2698

2699

2700
        /**
2701
         * Concatenates the string representation of all elements.
2702
         *
2703
         * Objects that implement __toString() does also work, otherwise (and in case
2704
         * of arrays) a PHP notice is generated. NULL and FALSE values are treated as
2705
         * empty strings.
2706
         *
2707
         * Examples:
2708
         *  Map::from( ['a', 'b', false] )->join();
2709
         *  Map::from( ['a', 'b', null, false] )->join( '-' );
2710
         *
2711
         * Results:
2712
         * The first example will return "ab" while the second one will return "a-b--"
2713
         *
2714
         * @param string $glue Character or string added between elements
2715
         * @return string String of concatenated map elements
2716
         */
2717
        public function join( string $glue = '' ) : string
2718
        {
2719
                return implode( $glue, $this->list() );
8✔
2720
        }
2721

2722

2723
        /**
2724
         * Specifies the data which should be serialized to JSON by json_encode().
2725
         *
2726
         * Examples:
2727
         *   json_encode( Map::from( ['a', 'b'] ) );
2728
         *   json_encode( Map::from( ['a' => 0, 'b' => 1] ) );
2729
         *
2730
         * Results:
2731
         *   ["a", "b"]
2732
         *   {"a":0,"b":1}
2733
         *
2734
         * @return array<int|string,mixed> Data to serialize to JSON
2735
         */
2736
        #[\ReturnTypeWillChange]
2737
        public function jsonSerialize()
2738
        {
2739
                return $this->list = $this->array( $this->list );
8✔
2740
        }
2741

2742

2743
        /**
2744
         * Returns the keys of the all elements in a new map object.
2745
         *
2746
         * Examples:
2747
         *  Map::from( ['a', 'b'] );
2748
         *  Map::from( ['a' => 0, 'b' => 1] );
2749
         *
2750
         * Results:
2751
         * The first example returns a map containing [0, 1] while the second one will
2752
         * return a map with ['a', 'b'].
2753
         *
2754
         * @return self<int|string,mixed> New map
2755
         */
2756
        public function keys() : self
2757
        {
2758
                return new static( array_keys( $this->list() ) );
8✔
2759
        }
2760

2761

2762
        /**
2763
         * Sorts the elements by their keys in reverse order.
2764
         *
2765
         * Examples:
2766
         *  Map::from( ['b' => 0, 'a' => 1] )->krsort();
2767
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsort();
2768
         *
2769
         * Results:
2770
         *  ['a' => 1, 'b' => 0]
2771
         *  [0 => 'b', 1 => 'a']
2772
         *
2773
         * The parameter modifies how the keys are compared. Possible values are:
2774
         * - SORT_REGULAR : compare elements normally (don't change types)
2775
         * - SORT_NUMERIC : compare elements numerically
2776
         * - SORT_STRING : compare elements as strings
2777
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2778
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2779
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2780
         *
2781
         * The keys are preserved using this method and no new map is created.
2782
         *
2783
         * @param int $options Sort options for krsort()
2784
         * @return self<int|string,mixed> Updated map for fluid interface
2785
         */
2786
        public function krsort( int $options = SORT_REGULAR ) : self
2787
        {
2788
                krsort( $this->list(), $options );
24✔
2789
                return $this;
24✔
2790
        }
2791

2792

2793
        /**
2794
         * Sorts a copy of the elements by their keys in reverse order.
2795
         *
2796
         * Examples:
2797
         *  Map::from( ['b' => 0, 'a' => 1] )->krsorted();
2798
         *  Map::from( [1 => 'a', 0 => 'b'] )->krsorted();
2799
         *
2800
         * Results:
2801
         *  ['a' => 1, 'b' => 0]
2802
         *  [0 => 'b', 1 => 'a']
2803
         *
2804
         * The parameter modifies how the keys are compared. Possible values are:
2805
         * - SORT_REGULAR : compare elements normally (don't change types)
2806
         * - SORT_NUMERIC : compare elements numerically
2807
         * - SORT_STRING : compare elements as strings
2808
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2809
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2810
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2811
         *
2812
         * The keys are preserved using this method and a new map is created.
2813
         *
2814
         * @param int $options Sort options for krsort()
2815
         * @return self<int|string,mixed> Updated map for fluid interface
2816
         */
2817
        public function krsorted( int $options = SORT_REGULAR ) : self
2818
        {
2819
                return ( clone $this )->krsort();
8✔
2820
        }
2821

2822

2823
        /**
2824
         * Sorts the elements by their keys.
2825
         *
2826
         * Examples:
2827
         *  Map::from( ['b' => 0, 'a' => 1] )->ksort();
2828
         *  Map::from( [1 => 'a', 0 => 'b'] )->ksort();
2829
         *
2830
         * Results:
2831
         *  ['a' => 1, 'b' => 0]
2832
         *  [0 => 'b', 1 => 'a']
2833
         *
2834
         * The parameter modifies how the keys are compared. Possible values are:
2835
         * - SORT_REGULAR : compare elements normally (don't change types)
2836
         * - SORT_NUMERIC : compare elements numerically
2837
         * - SORT_STRING : compare elements as strings
2838
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
2839
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
2840
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
2841
         *
2842
         * The keys are preserved using this method and no new map is created.
2843
         *
2844
         * @param int $options Sort options for ksort()
2845
         * @return self<int|string,mixed> Updated map for fluid interface
2846
         */
2847
        public function ksort( int $options = SORT_REGULAR ) : self
2848
        {
2849
                ksort( $this->list(), $options );
16✔
2850
                return $this;
16✔
2851
        }
2852

2853

2854
        /**
2855
         * Returns the last element from the map.
2856
         *
2857
         * Examples:
2858
         *  Map::from( ['a', 'b'] )->last();
2859
         *  Map::from( [] )->last( 'x' );
2860
         *  Map::from( [] )->last( new \Exception( 'error' ) );
2861
         *  Map::from( [] )->last( function() { return rand(); } );
2862
         *
2863
         * Results:
2864
         * The first example will return 'b' and the second one 'x'. The third example
2865
         * will throw the exception passed if the map contains no elements. In the
2866
         * fourth example, a random value generated by the closure function will be
2867
         * returned.
2868
         *
2869
         * @param mixed $default Default value or exception if the map contains no elements
2870
         * @return mixed Last value of map, (generated) default value or an exception
2871
         */
2872
        public function last( $default = null )
2873
        {
2874
                if( ( $value = end( $this->list() ) ) !== false ) {
40✔
2875
                        return $value;
16✔
2876
                }
2877

2878
                if( $default instanceof \Closure ) {
24✔
2879
                        return $default();
8✔
2880
                }
2881

2882
                if( $default instanceof \Throwable ) {
16✔
2883
                        throw $default;
8✔
2884
                }
2885

2886
                return $default;
8✔
2887
        }
2888

2889

2890
        /**
2891
         * Returns the last key from the map.
2892
         *
2893
         * Examples:
2894
         *  Map::from( ['a' => 1, 'b' => 2] )->lastKey();
2895
         *  Map::from( [] )->lastKey();
2896
         *
2897
         * Results:
2898
         * The first example will return 'b' and the second one NULL.
2899
         *
2900
         * @return mixed Last key of map or NULL if empty
2901
         */
2902
        public function lastKey()
2903
        {
2904
                $list = $this->list();
16✔
2905

2906
                if( function_exists( 'array_key_last' ) ) {
16✔
2907
                        return array_key_last( $list );
16✔
2908
                }
2909

2910
                end( $list );
×
2911
                return key( $list );
×
2912
        }
2913

2914

2915
        /**
2916
         * Removes the passed characters from the left of all strings.
2917
         *
2918
         * Examples:
2919
         *  Map::from( [" abc\n", "\tcde\r\n"] )->ltrim();
2920
         *  Map::from( ["a b c", "cbxa"] )->ltrim( 'abc' );
2921
         *
2922
         * Results:
2923
         * The first example will return ["abc\n", "cde\r\n"] while the second one will return [" b c", "xa"].
2924
         *
2925
         * @param string $chars List of characters to trim
2926
         * @return self<int|string,mixed> Updated map for fluid interface
2927
         */
2928
        public function ltrim( string $chars = " \n\r\t\v\x00" ) : self
2929
        {
2930
                foreach( $this->list() as &$entry )
8✔
2931
                {
2932
                        if( is_string( $entry ) ) {
8✔
2933
                                $entry = ltrim( $entry, $chars );
8✔
2934
                        }
2935
                }
2936

2937
                return $this;
8✔
2938
        }
2939

2940

2941
        /**
2942
         * Maps new values to the existing keys using the passed function and returns a new map for the result.
2943
         *
2944
         * Examples:
2945
         *  Map::from( ['a' => 2, 'b' => 4] )->map( function( $value, $key ) {
2946
         *      return $value * 2;
2947
         *  } );
2948
         *
2949
         * Results:
2950
         *  ['a' => 4, 'b' => 8]
2951
         *
2952
         * The keys are preserved using this method.
2953
         *
2954
         * @param callable $callback Function with (value, key) parameters and returns computed result
2955
         * @return self<int|string,mixed> New map with the original keys and the computed values
2956
         * @see rekey() - Changes the keys according to the passed function
2957
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
2958
         */
2959
        public function map( callable $callback ) : self
2960
        {
2961
                $list = $this->list();
8✔
2962
                $keys = array_keys( $list );
8✔
2963
                $elements = array_map( $callback, $list, $keys );
8✔
2964

2965
                return new static( array_combine( $keys, $elements ) ?: [] );
8✔
2966
        }
2967

2968

2969
        /**
2970
         * Returns the maximum value of all elements.
2971
         *
2972
         * Examples:
2973
         *  Map::from( [1, 3, 2, 5, 4] )->max()
2974
         *  Map::from( ['bar', 'foo', 'baz'] )->max()
2975
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->max( 'p' )
2976
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->max( 'i/p' )
2977
         *  Map::from( [50, 10, 30] )->max( fn( $val, $key ) => $key > 0 )
2978
         *
2979
         * Results:
2980
         * The first line will return "5", the second one "foo" and the third/fourth
2981
         * one return both 50 while the last one will return 30.
2982
         *
2983
         * This does also work for multi-dimensional arrays by passing the keys
2984
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
2985
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
2986
         * public properties of objects or objects implementing __isset() and __get() methods.
2987
         *
2988
         * Be careful comparing elements of different types because this can have
2989
         * unpredictable results due to the PHP comparison rules:
2990
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
2991
         *
2992
         * @param Closure|string|null $col Closure, key or path to the value of the nested array or object
2993
         * @return mixed Maximum value or NULL if there are no elements in the map
2994
         */
2995
        public function max( $col = null )
2996
        {
2997
                if( $col instanceof \Closure ) {
32✔
2998
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
2999
                } elseif( is_string( $col ) ) {
24✔
3000
                        $vals = $this->col( $col )->toArray();
8✔
3001
                } elseif( is_null( $col ) ) {
16✔
3002
                        $vals = $this->list();
16✔
3003
                } else {
3004
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
3005
                }
3006

3007
                return !empty( $vals ) ? max( $vals ) : null;
32✔
3008
        }
3009

3010

3011
        /**
3012
         * Merges the map with the given elements without returning a new map.
3013
         *
3014
         * Elements with the same non-numeric keys will be overwritten, elements
3015
         * with the same numeric keys will be added.
3016
         *
3017
         * Examples:
3018
         *  Map::from( ['a', 'b'] )->merge( ['b', 'c'] );
3019
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6] );
3020
         *  Map::from( ['a' => 1, 'b' => 2] )->merge( ['b' => 4, 'c' => 6], true );
3021
         *
3022
         * Results:
3023
         *  ['a', 'b', 'b', 'c']
3024
         *  ['a' => 1, 'b' => 4, 'c' => 6]
3025
         *  ['a' => 1, 'b' => [2, 4], 'c' => 6]
3026
         *
3027
         * The method is similar to replace() but doesn't replace elements with
3028
         * the same numeric keys. If you want to be sure that all passed elements
3029
         * are added without replacing existing ones, use concat() instead.
3030
         *
3031
         * The keys are preserved using this method.
3032
         *
3033
         * @param iterable<int|string,mixed> $elements List of elements
3034
         * @param bool $recursive TRUE to merge nested arrays too, FALSE for first level elements only
3035
         * @return self<int|string,mixed> Updated map for fluid interface
3036
         */
3037
        public function merge( iterable $elements, bool $recursive = false ) : self
3038
        {
3039
                if( $recursive ) {
24✔
3040
                        $this->list = array_merge_recursive( $this->list(), $this->array( $elements ) );
8✔
3041
                } else {
3042
                        $this->list = array_merge( $this->list(), $this->array( $elements ) );
16✔
3043
                }
3044

3045
                return $this;
24✔
3046
        }
3047

3048

3049
        /**
3050
         * Returns the minimum value of all elements.
3051
         *
3052
         * Examples:
3053
         *  Map::from( [2, 3, 1, 5, 4] )->min()
3054
         *  Map::from( ['baz', 'foo', 'bar'] )->min()
3055
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->min( 'p' )
3056
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->min( 'i/p' )
3057
         *  Map::from( [10, 50, 30] )->min( fn( $val, $key ) => $key > 0 )
3058
         *
3059
         * Results:
3060
         * The first line will return "1", the second one "bar", the third one
3061
         * 10, the forth and last one 30.
3062
         *
3063
         * This does also work for multi-dimensional arrays by passing the keys
3064
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
3065
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
3066
         * public properties of objects or objects implementing __isset() and __get() methods.
3067
         *
3068
         * Be careful comparing elements of different types because this can have
3069
         * unpredictable results due to the PHP comparison rules:
3070
         * {@link https://www.php.net/manual/en/language.operators.comparison.php}
3071
         *
3072
         * @param Closure|string|null $key Closure, key or path to the value of the nested array or object
3073
         * @return mixed Minimum value or NULL if there are no elements in the map
3074
         */
3075
        public function min( $col = null )
3076
        {
3077
                if( $col instanceof \Closure ) {
32✔
3078
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
3079
                } elseif( is_string( $col ) ) {
24✔
3080
                        $vals = $this->col( $col )->toArray();
8✔
3081
                } elseif( is_null( $col ) ) {
16✔
3082
                        $vals = $this->list();
16✔
3083
                } else {
3084
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
3085
                }
3086

3087
                return !empty( $vals ) ? min( $vals ) : null;
32✔
3088
        }
3089

3090

3091
        /**
3092
         * Tests if none of the elements are part of the map.
3093
         *
3094
         * Examples:
3095
         *  Map::from( ['a', 'b'] )->none( 'x' );
3096
         *  Map::from( ['a', 'b'] )->none( ['x', 'y'] );
3097
         *  Map::from( ['1', '2'] )->none( 2, true );
3098
         *  Map::from( ['a', 'b'] )->none( 'a' );
3099
         *  Map::from( ['a', 'b'] )->none( ['a', 'b'] );
3100
         *  Map::from( ['a', 'b'] )->none( ['a', 'x'] );
3101
         *
3102
         * Results:
3103
         * The first three examples will return TRUE while the other ones will return FALSE
3104
         *
3105
         * @param mixed|array $element Element or elements to search for in the map
3106
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
3107
         * @return bool TRUE if none of the elements is part of the map, FALSE if at least one is
3108
         */
3109
        public function none( $element, bool $strict = false ) : bool
3110
        {
3111
                $list = $this->list();
8✔
3112

3113
                if( !is_array( $element ) ) {
8✔
3114
                        return !in_array( $element, $list, $strict );
8✔
3115
                };
3116

3117
                foreach( $element as $entry )
8✔
3118
                {
3119
                        if( in_array( $entry, $list, $strict ) === true ) {
8✔
3120
                                return false;
8✔
3121
                        }
3122
                }
3123

3124
                return true;
8✔
3125
        }
3126

3127

3128
        /**
3129
         * Returns every nth element from the map.
3130
         *
3131
         * Examples:
3132
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2 );
3133
         *  Map::from( ['a', 'b', 'c', 'd', 'e', 'f'] )->nth( 2, 1 );
3134
         *
3135
         * Results:
3136
         *  ['a', 'c', 'e']
3137
         *  ['b', 'd', 'f']
3138
         *
3139
         * @param int $step Step width
3140
         * @param int $offset Number of element to start from (0-based)
3141
         * @return self<int|string,mixed> New map
3142
         */
3143
        public function nth( int $step, int $offset = 0 ) : self
3144
        {
3145
                $pos = 0;
8✔
3146
                $result = [];
8✔
3147

3148
                foreach( $this->list() as $key => $item )
8✔
3149
                {
3150
                        if( $pos++ % $step === $offset ) {
8✔
3151
                                $result[$key] = $item;
8✔
3152
                        }
3153
                }
3154

3155
                return new static( $result );
8✔
3156
        }
3157

3158

3159
        /**
3160
         * Determines if an element exists at an offset.
3161
         *
3162
         * Examples:
3163
         *  $map = Map::from( ['a' => 1, 'b' => 3, 'c' => null] );
3164
         *  isset( $map['b'] );
3165
         *  isset( $map['c'] );
3166
         *  isset( $map['d'] );
3167
         *
3168
         * Results:
3169
         *  The first isset() will return TRUE while the second and third one will return FALSE
3170
         *
3171
         * @param int|string $key Key to check for
3172
         * @return bool TRUE if key exists, FALSE if not
3173
         */
3174
        public function offsetExists( $key ) : bool
3175
        {
3176
                return isset( $this->list()[$key] );
56✔
3177
        }
3178

3179

3180
        /**
3181
         * Returns an element at a given offset.
3182
         *
3183
         * Examples:
3184
         *  $map = Map::from( ['a' => 1, 'b' => 3] );
3185
         *  $map['b'];
3186
         *
3187
         * Results:
3188
         *  $map['b'] will return 3
3189
         *
3190
         * @param int|string $key Key to return the element for
3191
         * @return mixed Value associated to the given key
3192
         */
3193
        #[\ReturnTypeWillChange]
3194
        public function offsetGet( $key )
3195
        {
3196
                return $this->list()[$key] ?? null;
40✔
3197
        }
3198

3199

3200
        /**
3201
         * Sets the element at a given offset.
3202
         *
3203
         * Examples:
3204
         *  $map = Map::from( ['a' => 1] );
3205
         *  $map['b'] = 2;
3206
         *  $map[0] = 4;
3207
         *
3208
         * Results:
3209
         *  ['a' => 1, 'b' => 2, 0 => 4]
3210
         *
3211
         * @param int|string|null $key Key to set the element for or NULL to append value
3212
         * @param mixed $value New value set for the key
3213
         */
3214
        public function offsetSet( $key, $value ) : void
3215
        {
3216
                if( $key !== null ) {
24✔
3217
                        $this->list()[$key] = $value;
16✔
3218
                } else {
3219
                        $this->list()[] = $value;
16✔
3220
                }
3221
        }
6✔
3222

3223

3224
        /**
3225
         * Unsets the element at a given offset.
3226
         *
3227
         * Examples:
3228
         *  $map = Map::from( ['a' => 1] );
3229
         *  unset( $map['a'] );
3230
         *
3231
         * Results:
3232
         *  The map will be empty
3233
         *
3234
         * @param int|string $key Key for unsetting the item
3235
         */
3236
        public function offsetUnset( $key ) : void
3237
        {
3238
                unset( $this->list()[$key] );
16✔
3239
        }
4✔
3240

3241

3242
        /**
3243
         * Returns a new map with only those elements specified by the given keys.
3244
         *
3245
         * Examples:
3246
         *  Map::from( ['a' => 1, 0 => 'b'] )->only( 'a' );
3247
         *  Map::from( ['a' => 1, 0 => 'b', 1 => 'c'] )->only( [0, 1] );
3248
         *
3249
         * Results:
3250
         *  ['a' => 1]
3251
         *  [0 => 'b', 1 => 'c']
3252
         *
3253
         * The keys are preserved using this method.
3254
         *
3255
         * @param iterable<mixed>|array<mixed>|string|int $keys Keys of the elements that should be returned
3256
         * @return self<int|string,mixed> New map with only the elements specified by the keys
3257
         */
3258
        public function only( $keys ) : self
3259
        {
3260
                return $this->intersectKeys( array_flip( $this->array( $keys ) ) );
8✔
3261
        }
3262

3263

3264
        /**
3265
         * Returns a new map with elements ordered by the passed keys.
3266
         *
3267
         * If there are less keys passed than available in the map, the remaining
3268
         * elements are removed. Otherwise, if keys are passed that are not in the
3269
         * map, they will be also available in the returned map but their value is
3270
         * NULL.
3271
         *
3272
         * Examples:
3273
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 'a'] );
3274
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1, 2] );
3275
         *  Map::from( ['a' => 1, 1 => 'c', 0 => 'b'] )->order( [0, 1] );
3276
         *
3277
         * Results:
3278
         *  [0 => 'b', 1 => 'c', 'a' => 1]
3279
         *  [0 => 'b', 1 => 'c', 2 => null]
3280
         *  [0 => 'b', 1 => 'c']
3281
         *
3282
         * The keys are preserved using this method.
3283
         *
3284
         * @param iterable<mixed> $keys Keys of the elements in the required order
3285
         * @return self<int|string,mixed> New map with elements ordered by the passed keys
3286
         */
3287
        public function order( iterable $keys ) : self
3288
        {
3289
                $result = [];
8✔
3290
                $list = $this->list();
8✔
3291

3292
                foreach( $this->array( $keys ) as $key ) {
8✔
3293
                        $result[$key] = $list[$key] ?? null;
8✔
3294
                }
3295

3296
                return new static( $result );
8✔
3297
        }
3298

3299

3300
        /**
3301
         * Fill up to the specified length with the given value
3302
         *
3303
         * In case the given number is smaller than the number of element that are
3304
         * already in the list, the map is unchanged. If the size is positive, the
3305
         * new elements are padded on the right, if it's negative then the elements
3306
         * are padded on the left.
3307
         *
3308
         * Examples:
3309
         *  Map::from( [1, 2, 3] )->pad( 5 );
3310
         *  Map::from( [1, 2, 3] )->pad( -5 );
3311
         *  Map::from( [1, 2, 3] )->pad( 5, '0' );
3312
         *  Map::from( [1, 2, 3] )->pad( 2 );
3313
         *  Map::from( [10 => 1, 20 => 2] )->pad( 3 );
3314
         *  Map::from( ['a' => 1, 'b' => 2] )->pad( 3, 3 );
3315
         *
3316
         * Results:
3317
         *  [1, 2, 3, null, null]
3318
         *  [null, null, 1, 2, 3]
3319
         *  [1, 2, 3, '0', '0']
3320
         *  [1, 2, 3]
3321
         *  [0 => 1, 1 => 2, 2 => null]
3322
         *  ['a' => 1, 'b' => 2, 0 => 3]
3323
         *
3324
         * Associative keys are preserved, numerical keys are replaced and numerical
3325
         * keys are used for the new elements.
3326
         *
3327
         * @param int $size Total number of elements that should be in the list
3328
         * @param mixed $value Value to fill up with if the map length is smaller than the given size
3329
         * @return self<int|string,mixed> New map
3330
         */
3331
        public function pad( int $size, $value = null ) : self
3332
        {
3333
                return new static( array_pad( $this->list(), $size, $value ) );
8✔
3334
        }
3335

3336

3337
        /**
3338
         * Breaks the list of elements into the given number of groups.
3339
         *
3340
         * Examples:
3341
         *  Map::from( [1, 2, 3, 4, 5] )->partition( 3 );
3342
         *  Map::from( [1, 2, 3, 4, 5] )->partition( function( $val, $idx ) {
3343
         *                return $idx % 3;
3344
         *        } );
3345
         *
3346
         * Results:
3347
         *  [[0 => 1, 1 => 2], [2 => 3, 3 => 4], [4 => 5]]
3348
         *  [0 => [0 => 1, 3 => 4], 1 => [1 => 2, 4 => 5], 2 => [2 => 3]]
3349
         *
3350
         * The keys of the original map are preserved in the returned map.
3351
         *
3352
         * @param \Closure|int $number Function with (value, index) as arguments returning the bucket key or number of groups
3353
         * @return self<int|string,mixed> New map
3354
         */
3355
        public function partition( $number ) : self
3356
        {
3357
                $list = $this->list();
32✔
3358

3359
                if( empty( $list ) ) {
32✔
3360
                        return new static();
8✔
3361
                }
3362

3363
                $result = [];
24✔
3364

3365
                if( $number instanceof \Closure )
24✔
3366
                {
3367
                        foreach( $list as $idx => $item ) {
8✔
3368
                                $result[$number( $item, $idx )][$idx] = $item;
8✔
3369
                        }
3370

3371
                        return new static( $result );
8✔
3372
                }
3373
                elseif( is_int( $number ) )
16✔
3374
                {
3375
                        $start = 0;
8✔
3376
                        $size = (int) ceil( count( $list ) / $number );
8✔
3377

3378
                        for( $i = 0; $i < $number; $i++ )
8✔
3379
                        {
3380
                                $result[] = array_slice( $list, $start, $size, true );
8✔
3381
                                $start += $size;
8✔
3382
                        }
3383

3384
                        return new static( $result );
8✔
3385
                }
3386

3387
                throw new \InvalidArgumentException( 'Parameter is no closure or integer' );
8✔
3388
        }
3389

3390

3391
        /**
3392
         * Returns the percentage of all elements passing the test in the map.
3393
         *
3394
         * Examples:
3395
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50 );
3396
         *  Map::from( [] )->percentage( fn( $val, $key ) => true );
3397
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 100 );
3398
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 3 );
3399
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val > 30, 0 );
3400
         *  Map::from( [30, 50, 10] )->percentage( fn( $val, $key ) => $val < 50, -1 );
3401
         *
3402
         * Results:
3403
         * The first line will return "66.67", the second and third one "0.0", the forth
3404
         * one "33.333", the fifth one "33.0" and the last one "70.0" (66 rounded up).
3405
         *
3406
         * @param Closure $fcn Closure to filter the values in the nested array or object to compute the percentage
3407
         * @param int $precision Number of decimal digits use by the result value
3408
         * @return float Percentage of all elements passing the test in the map
3409
         */
3410
        public function percentage( \Closure $fcn, int $precision = 2 ) : float
3411
        {
3412
                $vals = array_filter( $this->list(), $fcn, ARRAY_FILTER_USE_BOTH );
8✔
3413

3414
                $cnt = count( $this->list() );
8✔
3415
                return $cnt > 0 ? round( count( $vals ) * 100 / $cnt, $precision ) : 0;
8✔
3416
        }
3417

3418

3419
        /**
3420
         * Passes the map to the given callback and return the result.
3421
         *
3422
         * Examples:
3423
         *  Map::from( ['a', 'b'] )->pipe( function( $map ) {
3424
         *      return join( '-', $map->toArray() );
3425
         *  } );
3426
         *
3427
         * Results:
3428
         *  "a-b" will be returned
3429
         *
3430
         * @param \Closure $callback Function with map as parameter which returns arbitrary result
3431
         * @return mixed Result returned by the callback
3432
         */
3433
        public function pipe( \Closure $callback )
3434
        {
3435
                return $callback( $this );
8✔
3436
        }
3437

3438

3439
        /**
3440
         * Returns the values of a single column/property from an array of arrays or objects in a new map.
3441
         *
3442
         * This method is an alias for col(). For performance reasons, col() should
3443
         * be preferred because it uses one method call less than pluck().
3444
         *
3445
         * @param string|null $valuecol Name or path of the value property
3446
         * @param string|null $indexcol Name or path of the index property
3447
         * @return self<int|string,mixed> New map with mapped entries
3448
         * @see col() - Underlying method with same parameters and return value but better performance
3449
         */
3450
        public function pluck( ?string $valuecol = null, ?string $indexcol = null ) : self
3451
        {
3452
                return $this->col( $valuecol, $indexcol );
8✔
3453
        }
3454

3455

3456
        /**
3457
         * Returns and removes the last element from the map.
3458
         *
3459
         * Examples:
3460
         *  Map::from( ['a', 'b'] )->pop();
3461
         *
3462
         * Results:
3463
         *  "b" will be returned and the map only contains ['a'] afterwards
3464
         *
3465
         * @return mixed Last element of the map or null if empty
3466
         */
3467
        public function pop()
3468
        {
3469
                return array_pop( $this->list() );
16✔
3470
        }
3471

3472

3473
        /**
3474
         * Returns the numerical index of the value.
3475
         *
3476
         * Examples:
3477
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( 'b' );
3478
         *  Map::from( [4 => 'a', 8 => 'b'] )->pos( function( $item, $key ) {
3479
         *      return $item === 'b';
3480
         *  } );
3481
         *
3482
         * Results:
3483
         * Both examples will return "1" because the value "b" is at the second position
3484
         * and the returned index is zero based so the first item has the index "0".
3485
         *
3486
         * @param \Closure|mixed $value Value to search for or function with (item, key) parameters return TRUE if value is found
3487
         * @return int|null Position of the found value (zero based) or NULL if not found
3488
         */
3489
        public function pos( $value ) : ?int
3490
        {
3491
                $pos = 0;
120✔
3492
                $list = $this->list();
120✔
3493

3494
                if( $value instanceof \Closure )
120✔
3495
                {
3496
                        foreach( $list as $key => $item )
24✔
3497
                        {
3498
                                if( $value( $item, $key ) ) {
24✔
3499
                                        return $pos;
24✔
3500
                                }
3501

3502
                                ++$pos;
24✔
3503
                        }
3504
                }
3505

3506
                foreach( $list as $key => $item )
96✔
3507
                {
3508
                        if( $item === $value ) {
88✔
3509
                                return $pos;
80✔
3510
                        }
3511

3512
                        ++$pos;
48✔
3513
                }
3514

3515
                return null;
16✔
3516
        }
3517

3518

3519
        /**
3520
         * Adds a prefix in front of each map entry.
3521
         *
3522
         * By defaul, nested arrays are walked recusively so all entries at all levels are prefixed.
3523
         *
3524
         * Examples:
3525
         *  Map::from( ['a', 'b'] )->prefix( '1-' );
3526
         *  Map::from( ['a', ['b']] )->prefix( '1-' );
3527
         *  Map::from( ['a', ['b']] )->prefix( '1-', 1 );
3528
         *  Map::from( ['a', 'b'] )->prefix( function( $item, $key ) {
3529
         *      return ( ord( $item ) + ord( $key ) ) . '-';
3530
         *  } );
3531
         *
3532
         * Results:
3533
         *  The first example returns ['1-a', '1-b'] while the second one will return
3534
         *  ['1-a', ['1-b']]. In the third example, the depth is limited to the first
3535
         *  level only so it will return ['1-a', ['b']]. The forth example passing
3536
         *  the closure will return ['145-a', '147-b'].
3537
         *
3538
         * The keys of the original map are preserved in the returned map.
3539
         *
3540
         * @param \Closure|string $prefix Prefix string or anonymous function with ($item, $key) as parameters
3541
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
3542
         * @return self<int|string,mixed> Updated map for fluid interface
3543
         */
3544
        public function prefix( $prefix, ?int $depth = null ) : self
3545
        {
3546
                $fcn = function( array $list, $prefix, int $depth ) use ( &$fcn ) {
6✔
3547

3548
                        foreach( $list as $key => $item )
8✔
3549
                        {
3550
                                if( is_array( $item ) ) {
8✔
3551
                                        $list[$key] = $depth > 1 ? $fcn( $item, $prefix, $depth - 1 ) : $item;
8✔
3552
                                } else {
3553
                                        $list[$key] = ( is_callable( $prefix ) ? $prefix( $item, $key ) : $prefix ) . $item;
8✔
3554
                                }
3555
                        }
3556

3557
                        return $list;
8✔
3558
                };
8✔
3559

3560
                $this->list = $fcn( $this->list(), $prefix, $depth ?? 0x7fffffff );
8✔
3561
                return $this;
8✔
3562
        }
3563

3564

3565
        /**
3566
         * Pushes an element onto the beginning of the map without returning a new map.
3567
         *
3568
         * This method is an alias for unshift().
3569
         *
3570
         * @param mixed $value Item to add at the beginning
3571
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
3572
         * @return self<int|string,mixed> Updated map for fluid interface
3573
         * @see unshift() - Underlying method with same parameters and return value but better performance
3574
         */
3575
        public function prepend( $value, $key = null ) : self
3576
        {
3577
                return $this->unshift( $value, $key );
8✔
3578
        }
3579

3580

3581
        /**
3582
         * Returns and removes an element from the map by its key.
3583
         *
3584
         * Examples:
3585
         *  Map::from( ['a', 'b', 'c'] )->pull( 1 );
3586
         *  Map::from( ['a', 'b', 'c'] )->pull( 'x', 'none' );
3587
         *  Map::from( [] )->pull( 'Y', new \Exception( 'error' ) );
3588
         *  Map::from( [] )->pull( 'Z', function() { return rand(); } );
3589
         *
3590
         * Results:
3591
         * The first example will return "b" and the map contains ['a', 'c'] afterwards.
3592
         * The second one will return "none" and the map content stays untouched. If you
3593
         * pass an exception as default value, it will throw that exception if the map
3594
         * contains no elements. In the fourth example, a random value generated by the
3595
         * closure function will be returned.
3596
         *
3597
         * @param int|string $key Key to retrieve the value for
3598
         * @param mixed $default Default value if key isn't available
3599
         * @return mixed Value from map or default value
3600
         */
3601
        public function pull( $key, $default = null )
3602
        {
3603
                $value = $this->get( $key, $default );
32✔
3604
                unset( $this->list()[$key] );
24✔
3605

3606
                return $value;
24✔
3607
        }
3608

3609

3610
        /**
3611
         * Pushes an element onto the end of the map without returning a new map.
3612
         *
3613
         * Examples:
3614
         *  Map::from( ['a', 'b'] )->push( 'aa' );
3615
         *
3616
         * Results:
3617
         *  ['a', 'b', 'aa']
3618
         *
3619
         * @param mixed $value Value to add to the end
3620
         * @return self<int|string,mixed> Updated map for fluid interface
3621
         */
3622
        public function push( $value ) : self
3623
        {
3624
                $this->list()[] = $value;
24✔
3625
                return $this;
24✔
3626
        }
3627

3628

3629
        /**
3630
         * Sets the given key and value in the map without returning a new map.
3631
         *
3632
         * This method is an alias for set(). For performance reasons, set() should be
3633
         * preferred because it uses one method call less than put().
3634
         *
3635
         * @param int|string $key Key to set the new value for
3636
         * @param mixed $value New element that should be set
3637
         * @return self<int|string,mixed> Updated map for fluid interface
3638
         * @see set() - Underlying method with same parameters and return value but better performance
3639
         */
3640
        public function put( $key, $value ) : self
3641
        {
3642
                return $this->set( $key, $value );
8✔
3643
        }
3644

3645

3646
        /**
3647
         * Returns one or more random element from the map incl. their keys.
3648
         *
3649
         * Examples:
3650
         *  Map::from( [2, 4, 8, 16] )->random();
3651
         *  Map::from( [2, 4, 8, 16] )->random( 2 );
3652
         *  Map::from( [2, 4, 8, 16] )->random( 5 );
3653
         *
3654
         * Results:
3655
         * The first example will return a map including [0 => 8] or any other value,
3656
         * the second one will return a map with [0 => 16, 1 => 2] or any other values
3657
         * and the third example will return a map of the whole list in random order. The
3658
         * less elements are in the map, the less random the order will be, especially if
3659
         * the maximum number of values is high or close to the number of elements.
3660
         *
3661
         * The keys of the original map are preserved in the returned map.
3662
         *
3663
         * @param int $max Maximum number of elements that should be returned
3664
         * @return self<int|string,mixed> New map with key/element pairs from original map in random order
3665
         * @throws \InvalidArgumentException If requested number of elements is less than 1
3666
         */
3667
        public function random( int $max = 1 ) : self
3668
        {
3669
                if( $max < 1 ) {
40✔
3670
                        throw new \InvalidArgumentException( 'Requested number of elements must be greater or equal than 1' );
8✔
3671
                }
3672

3673
                $list = $this->list();
32✔
3674

3675
                if( empty( $list ) ) {
32✔
3676
                        return new static();
8✔
3677
                }
3678

3679
                if( ( $num = count( $list ) ) < $max ) {
24✔
3680
                        $max = $num;
8✔
3681
                }
3682

3683
                $keys = array_rand( $list, $max );
24✔
3684

3685
                return new static( array_intersect_key( $list, array_flip( (array) $keys ) ) );
24✔
3686
        }
3687

3688

3689
        /**
3690
         * Iteratively reduces the array to a single value using a callback function.
3691
         * Afterwards, the map will be empty.
3692
         *
3693
         * Examples:
3694
         *  Map::from( [2, 8] )->reduce( function( $result, $value ) {
3695
         *      return $result += $value;
3696
         *  }, 10 );
3697
         *
3698
         * Results:
3699
         *  "20" will be returned because the sum is computed by 10 (initial value) + 2 + 8
3700
         *
3701
         * @param callable $callback Function with (result, value) parameters and returns result
3702
         * @param mixed $initial Initial value when computing the result
3703
         * @return mixed Value computed by the callback function
3704
         */
3705
        public function reduce( callable $callback, $initial = null )
3706
        {
3707
                return array_reduce( $this->list(), $callback, $initial );
8✔
3708
        }
3709

3710

3711
        /**
3712
         * Removes all matched elements and returns a new map.
3713
         *
3714
         * Examples:
3715
         *  Map::from( [2 => 'a', 6 => 'b', 13 => 'm', 30 => 'z'] )->reject( function( $value, $key ) {
3716
         *      return $value < 'm';
3717
         *  } );
3718
         *  Map::from( [2 => 'a', 13 => 'm', 30 => 'z'] )->reject( 'm' );
3719
         *  Map::from( [2 => 'a', 6 => null, 13 => 'm'] )->reject();
3720
         *
3721
         * Results:
3722
         *  [13 => 'm', 30 => 'z']
3723
         *  [2 => 'a', 30 => 'z']
3724
         *  [6 => null]
3725
         *
3726
         * This method is the inverse of the filter() and should return TRUE if the
3727
         * item should be removed from the returned map.
3728
         *
3729
         * If no callback is passed, all values which are NOT empty, null or false will be
3730
         * removed. The keys of the original map are preserved in the returned map.
3731
         *
3732
         * @param Closure|mixed $callback Function with (item) parameter which returns TRUE/FALSE or value to compare with
3733
         * @return self<int|string,mixed> New map
3734
         */
3735
        public function reject( $callback = true ) : self
3736
        {
3737
                $isCallable = $callback instanceof \Closure;
24✔
3738

3739
                return new static( array_filter( $this->list(), function( $value, $key ) use  ( $callback, $isCallable ) {
18✔
3740
                        return $isCallable ? !$callback( $value, $key ) : $value != $callback;
24✔
3741
                }, ARRAY_FILTER_USE_BOTH ) );
24✔
3742
        }
3743

3744

3745
        /**
3746
         * Changes the keys according to the passed function.
3747
         *
3748
         * Examples:
3749
         *  Map::from( ['a' => 2, 'b' => 4] )->rekey( function( $value, $key ) {
3750
         *      return 'key-' . $key;
3751
         *  } );
3752
         *
3753
         * Results:
3754
         *  ['key-a' => 2, 'key-b' => 4]
3755
         *
3756
         * @param callable $callback Function with (value, key) parameters and returns new key
3757
         * @return self<int|string,mixed> New map with new keys and original values
3758
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
3759
         * @see transform() - Creates new key/value pairs using the passed function and returns a new map for the result
3760
         */
3761
        public function rekey( callable $callback ) : self
3762
        {
3763
                $list = $this->list();
8✔
3764
                $keys = array_keys( $list );
8✔
3765
                $newKeys = array_map( $callback, $list, $keys );
8✔
3766

3767
                return new static( array_combine( $newKeys, $list ) ?: [] );
8✔
3768
        }
3769

3770

3771
        /**
3772
         * Removes one or more elements from the map by its keys without returning a new map.
3773
         *
3774
         * Examples:
3775
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( 'a' );
3776
         *  Map::from( ['a' => 1, 2 => 'b'] )->remove( [2, 'a'] );
3777
         *
3778
         * Results:
3779
         * The first example will result in [2 => 'b'] while the second one resulting
3780
         * in an empty list
3781
         *
3782
         * @param iterable<string|int>|array<string|int>|string|int $keys List of keys to remove
3783
         * @return self<int|string,mixed> Updated map for fluid interface
3784
         */
3785
        public function remove( $keys ) : self
3786
        {
3787
                foreach( $this->array( $keys ) as $key ) {
64✔
3788
                        unset( $this->list()[$key] );
64✔
3789
                }
3790

3791
                return $this;
64✔
3792
        }
3793

3794

3795
        /**
3796
         * Replaces elements in the map with the given elements without returning a new map.
3797
         *
3798
         * Examples:
3799
         *  Map::from( ['a' => 1, 2 => 'b'] )->replace( ['a' => 2] );
3800
         *  Map::from( ['a' => 1, 'b' => ['c' => 3, 'd' => 4]] )->replace( ['b' => ['c' => 9]] );
3801
         *
3802
         * Results:
3803
         *  ['a' => 2, 2 => 'b']
3804
         *  ['a' => 1, 'b' => ['c' => 9, 'd' => 4]]
3805
         *
3806
         * The method is similar to merge() but it also replaces elements with numeric
3807
         * keys. These would be added by merge() with a new numeric key.
3808
         *
3809
         * The keys are preserved using this method.
3810
         *
3811
         * @param iterable<int|string,mixed> $elements List of elements
3812
         * @param bool $recursive TRUE to replace recursively (default), FALSE to replace elements only
3813
         * @return self<int|string,mixed> Updated map for fluid interface
3814
         */
3815
        public function replace( iterable $elements, bool $recursive = true ) : self
3816
        {
3817
                if( $recursive ) {
40✔
3818
                        $this->list = array_replace_recursive( $this->list(), $this->array( $elements ) );
32✔
3819
                } else {
3820
                        $this->list = array_replace( $this->list(), $this->array( $elements ) );
8✔
3821
                }
3822

3823
                return $this;
40✔
3824
        }
3825

3826

3827
        /**
3828
         * Reverses the element order with keys without returning a new map.
3829
         *
3830
         * Examples:
3831
         *  Map::from( ['a', 'b'] )->reverse();
3832
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reverse();
3833
         *
3834
         * Results:
3835
         *  ['b', 'a']
3836
         *  ['last' => 'user', 'name' => 'test']
3837
         *
3838
         * The keys are preserved using this method.
3839
         *
3840
         * @return self<int|string,mixed> Updated map for fluid interface
3841
         * @see reversed() - Reverses the element order in a copy of the map
3842
         */
3843
        public function reverse() : self
3844
        {
3845
                $this->list = array_reverse( $this->list(), true );
32✔
3846
                return $this;
32✔
3847
        }
3848

3849

3850
        /**
3851
         * Reverses the element order in a copy of the map.
3852
         *
3853
         * Examples:
3854
         *  Map::from( ['a', 'b'] )->reversed();
3855
         *  Map::from( ['name' => 'test', 'last' => 'user'] )->reversed();
3856
         *
3857
         * Results:
3858
         *  ['b', 'a']
3859
         *  ['last' => 'user', 'name' => 'test']
3860
         *
3861
         * The keys are preserved using this method and a new map is created before reversing the elements.
3862
         * Thus, reverse() should be preferred for performance reasons if possible.
3863
         *
3864
         * @return self<int|string,mixed> New map with a reversed copy of the elements
3865
         * @see reverse() - Reverses the element order with keys without returning a new map
3866
         */
3867
        public function reversed() : self
3868
        {
3869
                return ( clone $this )->reverse();
16✔
3870
        }
3871

3872

3873
        /**
3874
         * Sorts all elements in reverse order using new keys.
3875
         *
3876
         * Examples:
3877
         *  Map::from( ['a' => 1, 'b' => 0] )->rsort();
3878
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsort();
3879
         *
3880
         * Results:
3881
         *  [0 => 1, 1 => 0]
3882
         *  [0 => 'b', 1 => 'a']
3883
         *
3884
         * The parameter modifies how the values are compared. Possible parameter values are:
3885
         * - SORT_REGULAR : compare elements normally (don't change types)
3886
         * - SORT_NUMERIC : compare elements numerically
3887
         * - SORT_STRING : compare elements as strings
3888
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3889
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3890
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3891
         *
3892
         * The keys aren't preserved and elements get a new index. No new map is created
3893
         *
3894
         * @param int $options Sort options for rsort()
3895
         * @return self<int|string,mixed> Updated map for fluid interface
3896
         */
3897
        public function rsort( int $options = SORT_REGULAR ) : self
3898
        {
3899
                rsort( $this->list(), $options );
24✔
3900
                return $this;
24✔
3901
        }
3902

3903

3904
        /**
3905
         * Sorts a copy of all elements in reverse order using new keys.
3906
         *
3907
         * Examples:
3908
         *  Map::from( ['a' => 1, 'b' => 0] )->rsorted();
3909
         *  Map::from( [0 => 'b', 1 => 'a'] )->rsorted();
3910
         *
3911
         * Results:
3912
         *  [0 => 1, 1 => 0]
3913
         *  [0 => 'b', 1 => 'a']
3914
         *
3915
         * The parameter modifies how the values are compared. Possible parameter values are:
3916
         * - SORT_REGULAR : compare elements normally (don't change types)
3917
         * - SORT_NUMERIC : compare elements numerically
3918
         * - SORT_STRING : compare elements as strings
3919
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
3920
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
3921
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
3922
         *
3923
         * The keys aren't preserved, elements get a new index and a new map is created.
3924
         *
3925
         * @param int $options Sort options for rsort()
3926
         * @return self<int|string,mixed> Updated map for fluid interface
3927
         */
3928
        public function rsorted( int $options = SORT_REGULAR ) : self
3929
        {
3930
                return ( clone $this )->rsort( $options );
8✔
3931
        }
3932

3933

3934
        /**
3935
         * Removes the passed characters from the right of all strings.
3936
         *
3937
         * Examples:
3938
         *  Map::from( [" abc\n", "\tcde\r\n"] )->rtrim();
3939
         *  Map::from( ["a b c", "cbxa"] )->rtrim( 'abc' );
3940
         *
3941
         * Results:
3942
         * The first example will return [" abc", "\tcde"] while the second one will return ["a b ", "cbx"].
3943
         *
3944
         * @param string $chars List of characters to trim
3945
         * @return self<int|string,mixed> Updated map for fluid interface
3946
         */
3947
        public function rtrim( string $chars = " \n\r\t\v\x00" ) : self
3948
        {
3949
                foreach( $this->list() as &$entry )
8✔
3950
                {
3951
                        if( is_string( $entry ) ) {
8✔
3952
                                $entry = rtrim( $entry, $chars );
8✔
3953
                        }
3954
                }
3955

3956
                return $this;
8✔
3957
        }
3958

3959

3960
        /**
3961
         * Searches the map for a given value and return the corresponding key if successful.
3962
         *
3963
         * Examples:
3964
         *  Map::from( ['a', 'b', 'c'] )->search( 'b' );
3965
         *  Map::from( [1, 2, 3] )->search( '2', true );
3966
         *
3967
         * Results:
3968
         * The first example will return 1 (array index) while the second one will
3969
         * return NULL because the types doesn't match (int vs. string)
3970
         *
3971
         * @param mixed $value Item to search for
3972
         * @param bool $strict TRUE if type of the element should be checked too
3973
         * @return int|string|null Key associated to the value or null if not found
3974
         */
3975
        public function search( $value, $strict = true )
3976
        {
3977
                if( ( $result = array_search( $value, $this->list(), $strict ) ) !== false ) {
8✔
3978
                        return $result;
8✔
3979
                }
3980

3981
                return null;
8✔
3982
        }
3983

3984

3985
        /**
3986
         * Sets the seperator for paths to values in multi-dimensional arrays or objects.
3987
         *
3988
         * This method only changes the separator for the current map instance. To
3989
         * change the separator for all maps created afterwards, use the static
3990
         * delimiter() method instead.
3991
         *
3992
         * Examples:
3993
         *  Map::from( ['foo' => ['bar' => 'baz']] )->sep( '/' )->get( 'foo/bar' );
3994
         *
3995
         * Results:
3996
         *  'baz'
3997
         *
3998
         * @param string $char Separator character, e.g. "." for "key.to.value" instead of "key/to/value"
3999
         * @return self<int|string,mixed> Same map for fluid interface
4000
         */
4001
        public function sep( string $char ) : self
4002
        {
4003
                $this->sep = $char;
8✔
4004
                return $this;
8✔
4005
        }
4006

4007

4008
        /**
4009
         * Sets an element in the map by key without returning a new map.
4010
         *
4011
         * Examples:
4012
         *  Map::from( ['a'] )->set( 1, 'b' );
4013
         *  Map::from( ['a'] )->set( 0, 'b' );
4014
         *
4015
         * Results:
4016
         *  ['a', 'b']
4017
         *  ['b']
4018
         *
4019
         * @param int|string $key Key to set the new value for
4020
         * @param mixed $value New element that should be set
4021
         * @return self<int|string,mixed> Updated map for fluid interface
4022
         */
4023
        public function set( $key, $value ) : self
4024
        {
4025
                $this->list()[(string) $key] = $value;
40✔
4026
                return $this;
40✔
4027
        }
4028

4029

4030
        /**
4031
         * Returns and removes the first element from the map.
4032
         *
4033
         * Examples:
4034
         *  Map::from( ['a', 'b'] )->shift();
4035
         *  Map::from( [] )->shift();
4036
         *
4037
         * Results:
4038
         * The first example returns "a" and shortens the map to ['b'] only while the
4039
         * second example will return NULL
4040
         *
4041
         * Performance note:
4042
         * The bigger the list, the higher the performance impact because shift()
4043
         * reindexes all existing elements. Usually, it's better to reverse() the list
4044
         * and pop() entries from the list afterwards if a significant number of elements
4045
         * should be removed from the list:
4046
         *
4047
         *  $map->reverse()->pop();
4048
         * instead of
4049
         *  $map->shift( 'a' );
4050
         *
4051
         * @return mixed|null Value from map or null if not found
4052
         */
4053
        public function shift()
4054
        {
4055
                return array_shift( $this->list() );
8✔
4056
        }
4057

4058

4059
        /**
4060
         * Shuffles the elements in the map without returning a new map.
4061
         *
4062
         * Examples:
4063
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle();
4064
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffle( true );
4065
         *
4066
         * Results:
4067
         * The map in the first example will contain "a" and "b" in random order and
4068
         * with new keys assigned. The second call will also return all values in
4069
         * random order but preserves the keys of the original list.
4070
         *
4071
         * @param bool $assoc True to preserve keys, false to assign new keys
4072
         * @return self<int|string,mixed> Updated map for fluid interface
4073
         * @see shuffled() - Shuffles the elements in a copy of the map
4074
         */
4075
        public function shuffle( bool $assoc = false ) : self
4076
        {
4077
                if( $assoc )
24✔
4078
                {
4079
                        $list = $this->list();
8✔
4080
                        $keys = array_keys( $list );
8✔
4081
                        shuffle( $keys );
8✔
4082
                        $items = [];
8✔
4083

4084
                        foreach( $keys as $key ) {
8✔
4085
                                $items[$key] = $list[$key];
8✔
4086
                        }
4087

4088
                        $this->list = $items;
8✔
4089
                }
4090
                else
4091
                {
4092
                        shuffle( $this->list() );
16✔
4093
                }
4094

4095
                return $this;
24✔
4096
        }
4097

4098

4099
        /**
4100
         * Shuffles the elements in a copy of the map.
4101
         *
4102
         * Examples:
4103
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled();
4104
         *  Map::from( [2 => 'a', 4 => 'b'] )->shuffled( true );
4105
         *
4106
         * Results:
4107
         * The map in the first example will contain "a" and "b" in random order and
4108
         * with new keys assigned. The second call will also return all values in
4109
         * random order but preserves the keys of the original list.
4110
         *
4111
         * @param bool $assoc True to preserve keys, false to assign new keys
4112
         * @return self<int|string,mixed> New map with a shuffled copy of the elements
4113
         * @see shuffle() - Shuffles the elements in the map without returning a new map
4114
         */
4115
        public function shuffled( bool $assoc = false ) : self
4116
        {
4117
                return ( clone $this )->shuffle( $assoc );
8✔
4118
        }
4119

4120

4121
        /**
4122
         * Returns a new map with the given number of items skipped.
4123
         *
4124
         * Examples:
4125
         *  Map::from( [1, 2, 3, 4] )->skip( 2 );
4126
         *  Map::from( [1, 2, 3, 4] )->skip( function( $item, $key ) {
4127
         *      return $item < 4;
4128
         *  } );
4129
         *
4130
         * Results:
4131
         *  [2 => 3, 3 => 4]
4132
         *  [3 => 4]
4133
         *
4134
         * The keys of the items returned in the new map are the same as in the original one.
4135
         *
4136
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
4137
         * @return self<int|string,mixed> New map
4138
         */
4139
        public function skip( $offset ) : self
4140
        {
4141
                if( is_scalar( $offset ) ) {
24✔
4142
                        return new static( array_slice( $this->list(), (int) $offset, null, true ) );
8✔
4143
                }
4144

4145
                if( is_callable( $offset ) )
16✔
4146
                {
4147
                        $idx = 0;
8✔
4148
                        $list = $this->list();
8✔
4149

4150
                        foreach( $list as $key => $item )
8✔
4151
                        {
4152
                                if( !$offset( $item, $key ) ) {
8✔
4153
                                        break;
8✔
4154
                                }
4155

4156
                                ++$idx;
8✔
4157
                        }
4158

4159
                        return new static( array_slice( $list, $idx, null, true ) );
8✔
4160
                }
4161

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

4165

4166
        /**
4167
         * Returns a map with the slice from the original map.
4168
         *
4169
         * Examples:
4170
         *  Map::from( ['a', 'b', 'c'] )->slice( 1 );
4171
         *  Map::from( ['a', 'b', 'c'] )->slice( 1, 1 );
4172
         *  Map::from( ['a', 'b', 'c', 'd'] )->slice( -2, -1 );
4173
         *
4174
         * Results:
4175
         * The first example will return ['b', 'c'] and the second one ['b'] only.
4176
         * The third example returns ['c'] because the slice starts at the second
4177
         * last value and ends before the last value.
4178
         *
4179
         * The rules for offsets are:
4180
         * - If offset is non-negative, the sequence will start at that offset
4181
         * - If offset is negative, the sequence will start that far from the end
4182
         *
4183
         * Similar for the length:
4184
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4185
         * - If the array is shorter than the length, then only the available array elements will be present
4186
         * - If length is given and is negative then the sequence will stop that many elements from the end
4187
         * - If it is omitted, then the sequence will have everything from offset up until the end
4188
         *
4189
         * The keys of the items returned in the new map are the same as in the original one.
4190
         *
4191
         * @param int $offset Number of elements to start from
4192
         * @param int|null $length Number of elements to return or NULL for no limit
4193
         * @return self<int|string,mixed> New map
4194
         */
4195
        public function slice( int $offset, ?int $length = null ) : self
4196
        {
4197
                return new static( array_slice( $this->list(), $offset, $length, true ) );
48✔
4198
        }
4199

4200

4201
        /**
4202
         * Tests if at least one element passes the test or is part of the map.
4203
         *
4204
         * Examples:
4205
         *  Map::from( ['a', 'b'] )->some( 'a' );
4206
         *  Map::from( ['a', 'b'] )->some( ['a', 'c'] );
4207
         *  Map::from( ['a', 'b'] )->some( function( $item, $key ) {
4208
         *    return $item === 'a'
4209
         *  } );
4210
         *  Map::from( ['a', 'b'] )->some( ['c', 'd'] );
4211
         *  Map::from( ['1', '2'] )->some( [2], true );
4212
         *
4213
         * Results:
4214
         * The first three examples will return TRUE while the fourth and fifth will return FALSE
4215
         *
4216
         * @param \Closure|iterable|mixed $values Anonymous function with (item, key) parameter, element or list of elements to test against
4217
         * @param bool $strict TRUE to check the type too, using FALSE '1' and 1 will be the same
4218
         * @return bool TRUE if at least one element is available in map, FALSE if the map contains none of them
4219
         */
4220
        public function some( $values, bool $strict = false ) : bool
4221
        {
4222
                $list = $this->list();
48✔
4223

4224
                if( is_iterable( $values ) )
48✔
4225
                {
4226
                        foreach( $values as $entry )
24✔
4227
                        {
4228
                                if( in_array( $entry, $list, $strict ) === true ) {
24✔
4229
                                        return true;
24✔
4230
                                }
4231
                        }
4232

4233
                        return false;
16✔
4234
                }
4235
                elseif( is_callable( $values ) )
32✔
4236
                {
4237
                        foreach( $list as $key => $item )
16✔
4238
                        {
4239
                                if( $values( $item, $key ) ) {
16✔
4240
                                        return true;
16✔
4241
                                }
4242
                        }
4243
                }
4244
                elseif( in_array( $values, $list, $strict ) === true )
24✔
4245
                {
4246
                        return true;
24✔
4247
                }
4248

4249
                return false;
24✔
4250
        }
4251

4252

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

4284

4285
        /**
4286
         * Sorts the elements in a copy of the map using new keys.
4287
         *
4288
         * Examples:
4289
         *  Map::from( ['a' => 1, 'b' => 0] )->sorted();
4290
         *  Map::from( [0 => 'b', 1 => 'a'] )->sorted();
4291
         *
4292
         * Results:
4293
         *  [0 => 0, 1 => 1]
4294
         *  [0 => 'a', 1 => 'b']
4295
         *
4296
         * The parameter modifies how the values are compared. Possible parameter values are:
4297
         * - SORT_REGULAR : compare elements normally (don't change types)
4298
         * - SORT_NUMERIC : compare elements numerically
4299
         * - SORT_STRING : compare elements as strings
4300
         * - SORT_LOCALE_STRING : compare elements as strings, based on the current locale or changed by setlocale()
4301
         * - SORT_NATURAL : compare elements as strings using "natural ordering" like natsort()
4302
         * - SORT_FLAG_CASE : use SORT_STRING|SORT_FLAG_CASE and SORT_NATURALSORT_FLAG_CASE to sort strings case-insensitively
4303
         *
4304
         * The keys aren't preserved and elements get a new index and a new map is created before sorting the elements.
4305
         * Thus, sort() should be preferred for performance reasons if possible. A new map is created by calling this method.
4306
         *
4307
         * @param int $options Sort options for PHP sort()
4308
         * @return self<int|string,mixed> New map with a sorted copy of the elements
4309
         * @see sort() - Sorts elements in-place in the original map
4310
         */
4311
        public function sorted( int $options = SORT_REGULAR ) : self
4312
        {
4313
                return ( clone $this )->sort( $options );
16✔
4314
        }
4315

4316

4317
        /**
4318
         * Removes a portion of the map and replace it with the given replacement, then return the updated map.
4319
         *
4320
         * Examples:
4321
         *  Map::from( ['a', 'b', 'c'] )->splice( 1 );
4322
         *  Map::from( ['a', 'b', 'c'] )->splice( 1, 1, ['x', 'y'] );
4323
         *
4324
         * Results:
4325
         * The first example removes all entries after "a", so only ['a'] will be left
4326
         * in the map and ['b', 'c'] is returned. The second example replaces/returns "b"
4327
         * (start at 1, length 1) with ['x', 'y'] so the new map will contain
4328
         * ['a', 'x', 'y', 'c'] afterwards.
4329
         *
4330
         * The rules for offsets are:
4331
         * - If offset is non-negative, the sequence will start at that offset
4332
         * - If offset is negative, the sequence will start that far from the end
4333
         *
4334
         * Similar for the length:
4335
         * - If length is given and is positive, then the sequence will have up to that many elements in it
4336
         * - If the array is shorter than the length, then only the available array elements will be present
4337
         * - If length is given and is negative then the sequence will stop that many elements from the end
4338
         * - If it is omitted, then the sequence will have everything from offset up until the end
4339
         *
4340
         * Numerical array indexes are NOT preserved.
4341
         *
4342
         * @param int $offset Number of elements to start from
4343
         * @param int|null $length Number of elements to remove, NULL for all
4344
         * @param mixed $replacement List of elements to insert
4345
         * @return self<int|string,mixed> New map
4346
         */
4347
        public function splice( int $offset, ?int $length = null, $replacement = [] ) : self
4348
        {
4349
                if( $length === null ) {
40✔
4350
                        $length = count( $this->list() );
16✔
4351
                }
4352

4353
                return new static( array_splice( $this->list(), $offset, $length, (array) $replacement ) );
40✔
4354
        }
4355

4356

4357
        /**
4358
         * Returns the strings after the passed value.
4359
         *
4360
         * Examples:
4361
         *  Map::from( ['äöüß'] )->strAfter( 'ö' );
4362
         *  Map::from( ['abc'] )->strAfter( '' );
4363
         *  Map::from( ['abc'] )->strAfter( 'b' );
4364
         *  Map::from( ['abc'] )->strAfter( 'c' );
4365
         *  Map::from( ['abc'] )->strAfter( 'x' );
4366
         *  Map::from( [''] )->strAfter( '' );
4367
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4368
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4369
         *
4370
         * Results:
4371
         *  ['üß']
4372
         *  ['abc']
4373
         *  ['c']
4374
         *  ['']
4375
         *  []
4376
         *  []
4377
         *  ['1', '1', '1']
4378
         *  ['0', '0']
4379
         *
4380
         * All scalar values (bool, int, float, string) will be converted to strings.
4381
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4382
         *
4383
         * @param string $value Character or string to search for
4384
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4385
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4386
         * @return self<int|string,mixed> New map
4387
         */
4388
        public function strAfter( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4389
        {
4390
                $list = [];
8✔
4391
                $len = mb_strlen( $value );
8✔
4392
                $fcn = $case ? 'mb_stripos' : 'mb_strpos';
8✔
4393

4394
                foreach( $this->list() as $key => $entry )
8✔
4395
                {
4396
                        if( is_scalar( $entry ) )
8✔
4397
                        {
4398
                                $pos = null;
8✔
4399
                                $str = (string) $entry;
8✔
4400

4401
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4402
                                        $list[$key] = mb_substr( $str, $pos + $len, null, $encoding );
8✔
4403
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4404
                                        $list[$key] = $str;
8✔
4405
                                }
4406
                        }
4407
                }
4408

4409
                return new static( $list );
8✔
4410
        }
4411

4412

4413
        /**
4414
         * Returns the strings before the passed value.
4415
         *
4416
         * Examples:
4417
         *  Map::from( ['äöüß'] )->strBefore( 'ü' );
4418
         *  Map::from( ['abc'] )->strBefore( '' );
4419
         *  Map::from( ['abc'] )->strBefore( 'b' );
4420
         *  Map::from( ['abc'] )->strBefore( 'a' );
4421
         *  Map::from( ['abc'] )->strBefore( 'x' );
4422
         *  Map::from( [''] )->strBefore( '' );
4423
         *  Map::from( [1, 1.0, true, ['x'], new \stdClass] )->strAfter( '' );
4424
         *  Map::from( [0, 0.0, false, []] )->strAfter( '' );
4425
         *
4426
         * Results:
4427
         *  ['äö']
4428
         *  ['abc']
4429
         *  ['a']
4430
         *  ['']
4431
         *  []
4432
         *  []
4433
         *  ['1', '1', '1']
4434
         *  ['0', '0']
4435
         *
4436
         * All scalar values (bool, int, float, string) will be converted to strings.
4437
         * Non-scalar values as well as empty strings will be skipped and are not part of the result.
4438
         *
4439
         * @param string $value Character or string to search for
4440
         * @param bool $case TRUE if search should be case insensitive, FALSE if case-sensitive
4441
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4442
         * @return self<int|string,mixed> New map
4443
         */
4444
        public function strBefore( string $value, bool $case = false, string $encoding = 'UTF-8' ) : self
4445
        {
4446
                $list = [];
8✔
4447
                $fcn = $case ? 'mb_strripos' : 'mb_strrpos';
8✔
4448

4449
                foreach( $this->list() as $key => $entry )
8✔
4450
                {
4451
                        if( is_scalar( $entry ) )
8✔
4452
                        {
4453
                                $pos = null;
8✔
4454
                                $str = (string) $entry;
8✔
4455

4456
                                if( $str !== '' && $value !== '' && ( $pos = $fcn( $str, $value, 0, $encoding ) ) !== false ) {
8✔
4457
                                        $list[$key] = mb_substr( $str, 0, $pos, $encoding );
8✔
4458
                                } elseif( $str !== '' && $pos !== false ) {
8✔
4459
                                        $list[$key] = $str;
8✔
4460
                                } else {
1✔
4461
                                }
4462
                        }
4463
                }
4464

4465
                return new static( $list );
8✔
4466
        }
4467

4468

4469
        /**
4470
         * Tests if at least one of the passed strings is part of at least one entry.
4471
         *
4472
         * Examples:
4473
         *  Map::from( ['abc'] )->strContains( '' );
4474
         *  Map::from( ['abc'] )->strContains( 'a' );
4475
         *  Map::from( ['abc'] )->strContains( 'bc' );
4476
         *  Map::from( [12345] )->strContains( '23' );
4477
         *  Map::from( [123.4] )->strContains( 23.4 );
4478
         *  Map::from( [12345] )->strContains( false );
4479
         *  Map::from( [12345] )->strContains( true );
4480
         *  Map::from( [false] )->strContains( false );
4481
         *  Map::from( [''] )->strContains( false );
4482
         *  Map::from( ['abc'] )->strContains( ['b', 'd'] );
4483
         *  Map::from( ['abc'] )->strContains( 'c', 'ASCII' );
4484
         *
4485
         *  Map::from( ['abc'] )->strContains( 'd' );
4486
         *  Map::from( ['abc'] )->strContains( 'cb' );
4487
         *  Map::from( [23456] )->strContains( true );
4488
         *  Map::from( [false] )->strContains( 0 );
4489
         *  Map::from( ['abc'] )->strContains( ['d', 'e'] );
4490
         *  Map::from( ['abc'] )->strContains( 'cb', 'ASCII' );
4491
         *
4492
         * Results:
4493
         * The first eleven examples will return TRUE while the last six will return FALSE.
4494
         *
4495
         * @param array|string $value The string or list of strings to search for in each entry
4496
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4497
         * @return bool TRUE if one of the entries contains one of the strings, FALSE if not
4498
         */
4499
        public function strContains( $value, string $encoding = 'UTF-8' ) : bool
4500
        {
4501
                foreach( $this->list() as $entry )
8✔
4502
                {
4503
                        $entry = (string) $entry;
8✔
4504

4505
                        foreach( (array) $value as $str )
8✔
4506
                        {
4507
                                $str = (string) $str;
8✔
4508

4509
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4510
                                        return true;
8✔
4511
                                }
4512
                        }
4513
                }
4514

4515
                return false;
8✔
4516
        }
4517

4518

4519
        /**
4520
         * Tests if all of the entries contains one of the passed strings.
4521
         *
4522
         * Examples:
4523
         *  Map::from( ['abc', 'def'] )->strContainsAll( '' );
4524
         *  Map::from( ['abc', 'cba'] )->strContainsAll( 'a' );
4525
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'bc' );
4526
         *  Map::from( [12345, '230'] )->strContainsAll( '23' );
4527
         *  Map::from( [123.4, 23.42] )->strContainsAll( 23.4 );
4528
         *  Map::from( [12345, '234'] )->strContainsAll( [true, false] );
4529
         *  Map::from( ['', false] )->strContainsAll( false );
4530
         *  Map::from( ['abc', 'def'] )->strContainsAll( ['b', 'd'] );
4531
         *  Map::from( ['abc', 'ecf'] )->strContainsAll( 'c', 'ASCII' );
4532
         *
4533
         *  Map::from( ['abc', 'def'] )->strContainsAll( 'd' );
4534
         *  Map::from( ['abc', 'cab'] )->strContainsAll( 'cb' );
4535
         *  Map::from( [23456, '123'] )->strContainsAll( true );
4536
         *  Map::from( [false, '000'] )->strContainsAll( 0 );
4537
         *  Map::from( ['abc', 'acf'] )->strContainsAll( ['d', 'e'] );
4538
         *  Map::from( ['abc', 'bca'] )->strContainsAll( 'cb', 'ASCII' );
4539
         *
4540
         * Results:
4541
         * The first nine examples will return TRUE while the last six will return FALSE.
4542
         *
4543
         * @param array|string $value The string or list of strings to search for in each entry
4544
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4545
         * @return bool TRUE if all of the entries contains at least one of the strings, FALSE if not
4546
         */
4547
        public function strContainsAll( $value, string $encoding = 'UTF-8' ) : bool
4548
        {
4549
                $list = [];
8✔
4550

4551
                foreach( $this->list() as $entry )
8✔
4552
                {
4553
                        $entry = (string) $entry;
8✔
4554
                        $list[$entry] = 0;
8✔
4555

4556
                        foreach( (array) $value as $str )
8✔
4557
                        {
4558
                                $str = (string) $str;
8✔
4559

4560
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) !== false ) ) {
8✔
4561
                                        $list[$entry] = 1; break;
8✔
4562
                                }
4563
                        }
4564
                }
4565

4566
                return array_sum( $list ) === count( $list );
8✔
4567
        }
4568

4569

4570
        /**
4571
         * Tests if at least one of the entries ends with one of the passed strings.
4572
         *
4573
         * Examples:
4574
         *  Map::from( ['abc'] )->strEnds( '' );
4575
         *  Map::from( ['abc'] )->strEnds( 'c' );
4576
         *  Map::from( ['abc'] )->strEnds( 'bc' );
4577
         *  Map::from( ['abc'] )->strEnds( ['b', 'c'] );
4578
         *  Map::from( ['abc'] )->strEnds( 'c', 'ASCII' );
4579
         *  Map::from( ['abc'] )->strEnds( 'a' );
4580
         *  Map::from( ['abc'] )->strEnds( 'cb' );
4581
         *  Map::from( ['abc'] )->strEnds( ['d', 'b'] );
4582
         *  Map::from( ['abc'] )->strEnds( 'cb', 'ASCII' );
4583
         *
4584
         * Results:
4585
         * The first five examples will return TRUE while the last four will return FALSE.
4586
         *
4587
         * @param array|string $value The string or strings to search for in each entry
4588
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4589
         * @return bool TRUE if one of the entries ends with one of the strings, FALSE if not
4590
         */
4591
        public function strEnds( $value, string $encoding = 'UTF-8' ) : bool
4592
        {
4593
                foreach( $this->list() as $entry )
8✔
4594
                {
4595
                        $entry = (string) $entry;
8✔
4596

4597
                        foreach( (array) $value as $str )
8✔
4598
                        {
4599
                                $len = mb_strlen( (string) $str );
8✔
4600

4601
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4602
                                        return true;
8✔
4603
                                }
4604
                        }
4605
                }
4606

4607
                return false;
8✔
4608
        }
4609

4610

4611
        /**
4612
         * Tests if all of the entries ends with at least one of the passed strings.
4613
         *
4614
         * Examples:
4615
         *  Map::from( ['abc', 'def'] )->strEndsAll( '' );
4616
         *  Map::from( ['abc', 'bac'] )->strEndsAll( 'c' );
4617
         *  Map::from( ['abc', 'cbc'] )->strEndsAll( 'bc' );
4618
         *  Map::from( ['abc', 'def'] )->strEndsAll( ['c', 'f'] );
4619
         *  Map::from( ['abc', 'efc'] )->strEndsAll( 'c', 'ASCII' );
4620
         *  Map::from( ['abc', 'fed'] )->strEndsAll( 'd' );
4621
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca' );
4622
         *  Map::from( ['abc', 'acf'] )->strEndsAll( ['a', 'c'] );
4623
         *  Map::from( ['abc', 'bca'] )->strEndsAll( 'ca', 'ASCII' );
4624
         *
4625
         * Results:
4626
         * The first five examples will return TRUE while the last four will return FALSE.
4627
         *
4628
         * @param array|string $value The string or strings to search for in each entry
4629
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4630
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4631
         */
4632
        public function strEndsAll( $value, string $encoding = 'UTF-8' ) : bool
4633
        {
4634
                $list = [];
8✔
4635

4636
                foreach( $this->list() as $entry )
8✔
4637
                {
4638
                        $entry = (string) $entry;
8✔
4639
                        $list[$entry] = 0;
8✔
4640

4641
                        foreach( (array) $value as $str )
8✔
4642
                        {
4643
                                $len = mb_strlen( (string) $str );
8✔
4644

4645
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, -$len, $encoding ) !== false ) ) {
8✔
4646
                                        $list[$entry] = 1; break;
8✔
4647
                                }
4648
                        }
4649
                }
4650

4651
                return array_sum( $list ) === count( $list );
8✔
4652
        }
4653

4654

4655
        /**
4656
         * Returns an element by key and casts it to string if possible.
4657
         *
4658
         * Examples:
4659
         *  Map::from( ['a' => true] )->string( 'a' );
4660
         *  Map::from( ['a' => 1] )->string( 'a' );
4661
         *  Map::from( ['a' => 1.1] )->string( 'a' );
4662
         *  Map::from( ['a' => 'abc'] )->string( 'a' );
4663
         *  Map::from( ['a' => ['b' => ['c' => 'yes']]] )->string( 'a/b/c' );
4664
         *  Map::from( [] )->string( 'a', function() { return 'no'; } );
4665
         *
4666
         *  Map::from( [] )->string( 'b' );
4667
         *  Map::from( ['b' => ''] )->string( 'b' );
4668
         *  Map::from( ['b' => null] )->string( 'b' );
4669
         *  Map::from( ['b' => [true]] )->string( 'b' );
4670
         *  Map::from( ['b' => resource] )->string( 'b' );
4671
         *  Map::from( ['b' => new \stdClass] )->string( 'b' );
4672
         *
4673
         *  Map::from( [] )->string( 'c', new \Exception( 'error' ) );
4674
         *
4675
         * Results:
4676
         * The first six examples will return the value as string while the 9th to 12th
4677
         * example returns an empty string. The last example will throw an exception.
4678
         *
4679
         * This does also work for multi-dimensional arrays by passing the keys
4680
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
4681
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
4682
         * public properties of objects or objects implementing __isset() and __get() methods.
4683
         *
4684
         * @param int|string $key Key or path to the requested item
4685
         * @param mixed $default Default value if key isn't found (will be casted to bool)
4686
         * @return string Value from map or default value
4687
         */
4688
        public function string( $key, $default = '' ) : string
4689
        {
4690
                return (string) ( is_scalar( $val = $this->get( $key, $default ) ) ? $val : $default );
24✔
4691
        }
4692

4693

4694
        /**
4695
         * Converts all alphabetic characters in strings to lower case.
4696
         *
4697
         * Examples:
4698
         *  Map::from( ['My String'] )->strLower();
4699
         *  Map::from( ['Τάχιστη'] )->strLower();
4700
         *  Map::from( ['Äpfel', 'Birnen'] )->strLower( 'ISO-8859-1' );
4701
         *  Map::from( [123] )->strLower();
4702
         *  Map::from( [new stdClass] )->strLower();
4703
         *
4704
         * Results:
4705
         * The first example will return ["my string"], the second one ["τάχιστη"] and
4706
         * the third one ["äpfel", "birnen"]. The last two strings will be unchanged.
4707
         *
4708
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4709
         * @return self<int|string,mixed> Updated map for fluid interface
4710
         */
4711
        public function strLower( string $encoding = 'UTF-8' ) : self
4712
        {
4713
                foreach( $this->list() as &$entry )
8✔
4714
                {
4715
                        if( is_string( $entry ) ) {
8✔
4716
                                $entry = mb_strtolower( $entry, $encoding );
8✔
4717
                        }
4718
                }
4719

4720
                return $this;
8✔
4721
        }
4722

4723

4724
        /**
4725
         * Replaces all occurrences of the search string with the replacement string.
4726
         *
4727
         * Examples:
4728
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( '.com', '.de' );
4729
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], '.de' );
4730
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.de'] );
4731
         * Map::from( ['google.com', 'aimeos.org'] )->strReplace( ['.com', '.org'], ['.fr', '.de'] );
4732
         * Map::from( ['google.com', 'aimeos.com'] )->strReplace( ['.com', '.co'], ['.co', '.de', '.fr'] );
4733
         * Map::from( ['google.com', 'aimeos.com', 123] )->strReplace( '.com', '.de' );
4734
         * Map::from( ['GOOGLE.COM', 'AIMEOS.COM'] )->strReplace( '.com', '.de', true );
4735
         *
4736
         * Restults:
4737
         * ['google.de', 'aimeos.de']
4738
         * ['google.de', 'aimeos.de']
4739
         * ['google.de', 'aimeos']
4740
         * ['google.fr', 'aimeos.de']
4741
         * ['google.de', 'aimeos.de']
4742
         * ['google.de', 'aimeos.de', 123]
4743
         * ['GOOGLE.de', 'AIMEOS.de']
4744
         *
4745
         * If you use an array of strings for search or search/replacement, the order of
4746
         * the strings matters! Each search string found is replaced by the corresponding
4747
         * replacement string at the same position.
4748
         *
4749
         * In case of array parameters and if the number of replacement strings is less
4750
         * than the number of search strings, the search strings with no corresponding
4751
         * replacement string are replaced with empty strings. Replacement strings with
4752
         * no corresponding search string are ignored.
4753
         *
4754
         * An array parameter for the replacements is only allowed if the search parameter
4755
         * is an array of strings too!
4756
         *
4757
         * Because the method replaces from left to right, it might replace a previously
4758
         * inserted value when doing multiple replacements. Entries which are non-string
4759
         * values are left untouched.
4760
         *
4761
         * @param array|string $search String or list of strings to search for
4762
         * @param array|string $replace String or list of strings of replacement strings
4763
         * @param bool $case TRUE if replacements should be case insensitive, FALSE if case-sensitive
4764
         * @return self<int|string,mixed> Updated map for fluid interface
4765
         */
4766
        public function strReplace( $search, $replace, bool $case = false ) : self
4767
        {
4768
                $fcn = $case ? 'str_ireplace' : 'str_replace';
8✔
4769

4770
                foreach( $this->list() as &$entry )
8✔
4771
                {
4772
                        if( is_string( $entry ) ) {
8✔
4773
                                $entry = $fcn( $search, $replace, $entry );
8✔
4774
                        }
4775
                }
4776

4777
                return $this;
8✔
4778
        }
4779

4780

4781
        /**
4782
         * Tests if at least one of the entries starts with at least one of the passed strings.
4783
         *
4784
         * Examples:
4785
         *  Map::from( ['abc'] )->strStarts( '' );
4786
         *  Map::from( ['abc'] )->strStarts( 'a' );
4787
         *  Map::from( ['abc'] )->strStarts( 'ab' );
4788
         *  Map::from( ['abc'] )->strStarts( ['a', 'b'] );
4789
         *  Map::from( ['abc'] )->strStarts( 'ab', 'ASCII' );
4790
         *  Map::from( ['abc'] )->strStarts( 'b' );
4791
         *  Map::from( ['abc'] )->strStarts( 'bc' );
4792
         *  Map::from( ['abc'] )->strStarts( ['b', 'c'] );
4793
         *  Map::from( ['abc'] )->strStarts( 'bc', 'ASCII' );
4794
         *
4795
         * Results:
4796
         * The first five examples will return TRUE while the last four will return FALSE.
4797
         *
4798
         * @param array|string $value The string or strings to search for in each entry
4799
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4800
         * @return bool TRUE if all of the entries ends with at least one of the strings, FALSE if not
4801
         */
4802
        public function strStarts( $value, string $encoding = 'UTF-8' ) : bool
4803
        {
4804
                foreach( $this->list() as $entry )
8✔
4805
                {
4806
                        $entry = (string) $entry;
8✔
4807

4808
                        foreach( (array) $value as $str )
8✔
4809
                        {
4810
                                if( ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
4811
                                        return true;
8✔
4812
                                }
4813
                        }
4814
                }
4815

4816
                return false;
8✔
4817
        }
4818

4819

4820
        /**
4821
         * Tests if all of the entries starts with one of the passed strings.
4822
         *
4823
         * Examples:
4824
         *  Map::from( ['abc', 'def'] )->strStartsAll( '' );
4825
         *  Map::from( ['abc', 'acb'] )->strStartsAll( 'a' );
4826
         *  Map::from( ['abc', 'aba'] )->strStartsAll( 'ab' );
4827
         *  Map::from( ['abc', 'def'] )->strStartsAll( ['a', 'd'] );
4828
         *  Map::from( ['abc', 'acf'] )->strStartsAll( 'a', 'ASCII' );
4829
         *  Map::from( ['abc', 'def'] )->strStartsAll( 'd' );
4830
         *  Map::from( ['abc', 'bca'] )->strStartsAll( 'ab' );
4831
         *  Map::from( ['abc', 'bac'] )->strStartsAll( ['a', 'c'] );
4832
         *  Map::from( ['abc', 'cab'] )->strStartsAll( 'ab', 'ASCII' );
4833
         *
4834
         * Results:
4835
         * The first five examples will return TRUE while the last four will return FALSE.
4836
         *
4837
         * @param array|string $value The string or strings to search for in each entry
4838
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4839
         * @return bool TRUE if one of the entries starts with one of the strings, FALSE if not
4840
         */
4841
        public function strStartsAll( $value, string $encoding = 'UTF-8' ) : bool
4842
        {
4843
                $list = [];
8✔
4844

4845
                foreach( $this->list() as $entry )
8✔
4846
                {
4847
                        $entry = (string) $entry;
8✔
4848
                        $list[$entry] = 0;
8✔
4849

4850
                        foreach( (array) $value as $str )
8✔
4851
                        {
4852
                                if( (int) ( $str === '' || mb_strpos( $entry, (string) $str, 0, $encoding ) === 0 ) ) {
8✔
4853
                                        $list[$entry] = 1; break;
8✔
4854
                                }
4855
                        }
4856
                }
4857

4858
                return array_sum( $list ) === count( $list );
8✔
4859
        }
4860

4861

4862
        /**
4863
         * Converts all alphabetic characters in strings to upper case.
4864
         *
4865
         * Examples:
4866
         *  Map::from( ['My String'] )->strUpper();
4867
         *  Map::from( ['τάχιστη'] )->strUpper();
4868
         *  Map::from( ['äpfel', 'birnen'] )->strUpper( 'ISO-8859-1' );
4869
         *  Map::from( [123] )->strUpper();
4870
         *  Map::from( [new stdClass] )->strUpper();
4871
         *
4872
         * Results:
4873
         * The first example will return ["MY STRING"], the second one ["ΤΆΧΙΣΤΗ"] and
4874
         * the third one ["ÄPFEL", "BIRNEN"]. The last two strings will be unchanged.
4875
         *
4876
         * @param string $encoding Character encoding of the strings, e.g. "UTF-8" (default), "ASCII", "ISO-8859-1", etc.
4877
         * @return self<int|string,mixed> Updated map for fluid interface
4878
         */
4879
        public function strUpper( string $encoding = 'UTF-8' ) :self
4880
        {
4881
                foreach( $this->list() as &$entry )
8✔
4882
                {
4883
                        if( is_string( $entry ) ) {
8✔
4884
                                $entry = mb_strtoupper( $entry, $encoding );
8✔
4885
                        }
4886
                }
4887

4888
                return $this;
8✔
4889
        }
4890

4891

4892
        /**
4893
         * Adds a suffix at the end of each map entry.
4894
         *
4895
         * By defaul, nested arrays are walked recusively so all entries at all levels are suffixed.
4896
         *
4897
         * Examples:
4898
         *  Map::from( ['a', 'b'] )->suffix( '-1' );
4899
         *  Map::from( ['a', ['b']] )->suffix( '-1' );
4900
         *  Map::from( ['a', ['b']] )->suffix( '-1', 1 );
4901
         *  Map::from( ['a', 'b'] )->suffix( function( $item, $key ) {
4902
         *      return '-' . ( ord( $item ) + ord( $key ) );
4903
         *  } );
4904
         *
4905
         * Results:
4906
         *  The first example returns ['a-1', 'b-1'] while the second one will return
4907
         *  ['a-1', ['b-1']]. In the third example, the depth is limited to the first
4908
         *  level only so it will return ['a-1', ['b']]. The forth example passing
4909
         *  the closure will return ['a-145', 'b-147'].
4910
         *
4911
         * The keys are preserved using this method.
4912
         *
4913
         * @param \Closure|string $suffix Suffix string or anonymous function with ($item, $key) as parameters
4914
         * @param int|null $depth Maximum depth to dive into multi-dimensional arrays starting from "1"
4915
         * @return self<int|string,mixed> Updated map for fluid interface
4916
         */
4917
        public function suffix( $suffix, ?int $depth = null ) : self
4918
        {
4919
                $fcn = function( $list, $suffix, $depth ) use ( &$fcn ) {
6✔
4920

4921
                        foreach( $list as $key => $item )
8✔
4922
                        {
4923
                                if( is_array( $item ) ) {
8✔
4924
                                        $list[$key] = $depth > 1 ? $fcn( $item, $suffix, $depth - 1 ) : $item;
8✔
4925
                                } else {
4926
                                        $list[$key] = $item . ( is_callable( $suffix ) ? $suffix( $item, $key ) : $suffix );
8✔
4927
                                }
4928
                        }
4929

4930
                        return $list;
8✔
4931
                };
8✔
4932

4933
                $this->list = $fcn( $this->list(), $suffix, $depth ?? 0x7fffffff );
8✔
4934
                return $this;
8✔
4935
        }
4936

4937

4938
        /**
4939
         * Returns the sum of all integer and float values in the map.
4940
         *
4941
         * Examples:
4942
         *  Map::from( [1, 3, 5] )->sum();
4943
         *  Map::from( [1, 'sum', 5] )->sum();
4944
         *  Map::from( [['p' => 30], ['p' => 50], ['p' => 10]] )->sum( 'p' );
4945
         *  Map::from( [['i' => ['p' => 30]], ['i' => ['p' => 50]]] )->sum( 'i/p' );
4946
         *  Map::from( [30, 50, 10] )->sum( fn( $val, $key ) => $val < 50 );
4947
         *
4948
         * Results:
4949
         * The first line will return "9", the second one "6", the third one "90"
4950
         * the forth on "80" and the last one "40".
4951
         *
4952
         * NULL values are treated as 0, non-numeric values will generate an error.
4953
         *
4954
         * NULL values are treated as 0, non-numeric values will generate an error.
4955
         *
4956
         * This does also work for multi-dimensional arrays by passing the keys
4957
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
4958
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
4959
         * public properties of objects or objects implementing __isset() and __get() methods.
4960
         *
4961
         * @param Closure|string|null $col Closure, key or path to the values in the nested array or object to sum up
4962
         * @return float Sum of all elements or 0 if there are no elements in the map
4963
         */
4964
        public function sum( $col = null ) : float
4965
        {
4966
                if( $col instanceof \Closure ) {
24✔
4967
                        $vals = array_filter( $this->list(), $col, ARRAY_FILTER_USE_BOTH );
8✔
4968
                } elseif( is_string( $col ) ) {
16✔
4969
                        $vals = $this->col( $col )->toArray();
8✔
4970
                } elseif( is_null( $col ) ) {
8✔
4971
                        $vals = $this->list();
8✔
4972
                } else {
4973
                        throw new \InvalidArgumentException( 'Parameter is no closure or string' );
×
4974
                }
4975

4976
                return array_sum( $vals );
24✔
4977
        }
4978

4979

4980
        /**
4981
         * Returns a new map with the given number of items.
4982
         *
4983
         * The keys of the items returned in the new map are the same as in the original one.
4984
         *
4985
         * Examples:
4986
         *  Map::from( [1, 2, 3, 4] )->take( 2 );
4987
         *  Map::from( [1, 2, 3, 4] )->take( 2, 1 );
4988
         *  Map::from( [1, 2, 3, 4] )->take( 2, -2 );
4989
         *  Map::from( [1, 2, 3, 4] )->take( 2, function( $item, $key ) {
4990
         *      return $item < 2;
4991
         *  } );
4992
         *
4993
         * Results:
4994
         *  [0 => 1, 1 => 2]
4995
         *  [1 => 2, 2 => 3]
4996
         *  [2 => 3, 3 => 4]
4997
         *  [1 => 2, 2 => 3]
4998
         *
4999
         * The keys of the items returned in the new map are the same as in the original one.
5000
         *
5001
         * @param int $size Number of items to return
5002
         * @param \Closure|int $offset Number of items to skip or function($item, $key) returning true for skipped items
5003
         * @return self<int|string,mixed> New map
5004
         */
5005
        public function take( int $size, $offset = 0 ) : self
5006
        {
5007
                $list = $this->list();
40✔
5008

5009
                if( is_scalar( $offset ) ) {
40✔
5010
                        return new static( array_slice( $list, (int) $offset, $size, true ) );
24✔
5011
                }
5012

5013
                if( is_callable( $offset ) )
16✔
5014
                {
5015
                        $idx = 0;
8✔
5016

5017
                        foreach( $list as $key => $item )
8✔
5018
                        {
5019
                                if( !$offset( $item, $key ) ) {
8✔
5020
                                        break;
8✔
5021
                                }
5022

5023
                                ++$idx;
8✔
5024
                        }
5025

5026
                        return new static( array_slice( $list, $idx, $size, true ) );
8✔
5027
                }
5028

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

5032

5033
        /**
5034
         * Passes a clone of the map to the given callback.
5035
         *
5036
         * Use it to "tap" into a chain of methods to check the state between two
5037
         * method calls. The original map is not altered by anything done in the
5038
         * callback.
5039
         *
5040
         * Examples:
5041
         *  Map::from( [3, 2, 1] )->rsort()->tap( function( $map ) {
5042
         *    print_r( $map->remove( 0 )->toArray() );
5043
         *  } )->first();
5044
         *
5045
         * Results:
5046
         * It will sort the list in reverse order (`[1, 2, 3]`) while keeping the keys,
5047
         * then prints the items without the first (`[2, 3]`) in the function passed
5048
         * to `tap()` and returns the first item ("1") at the end.
5049
         *
5050
         * @param callable $callback Function receiving ($map) parameter
5051
         * @return self<int|string,mixed> Same map for fluid interface
5052
         */
5053
        public function tap( callable $callback ) : self
5054
        {
5055
                $callback( clone $this );
8✔
5056
                return $this;
8✔
5057
        }
5058

5059

5060
        /**
5061
         * Returns the elements as a plain array.
5062
         *
5063
         * @return array<int|string,mixed> Plain array
5064
         */
5065
        public function toArray() : array
5066
        {
5067
                return $this->list = $this->array( $this->list );
1,784✔
5068
        }
5069

5070

5071
        /**
5072
         * Returns the elements encoded as JSON string.
5073
         *
5074
         * There are several options available to modify the JSON output:
5075
         * {@link https://www.php.net/manual/en/function.json-encode.php}
5076
         * The parameter can be a single JSON_* constant or a bitmask of several
5077
         * constants combine by bitwise OR (|), e.g.:
5078
         *
5079
         *  JSON_FORCE_OBJECT|JSON_HEX_QUOT
5080
         *
5081
         * @param int $options Combination of JSON_* constants
5082
         * @return string|null Array encoded as JSON string or NULL on failure
5083
         */
5084
        public function toJson( int $options = 0 ) : ?string
5085
        {
5086
                $result = json_encode( $this->list(), $options );
16✔
5087
                return $result !== false ? $result : null;
16✔
5088
        }
5089

5090

5091
        /**
5092
         * Reverses the element order in a copy of the map (alias).
5093
         *
5094
         * This method is an alias for reversed(). For performance reasons, reversed() should be
5095
         * preferred because it uses one method call less than toReversed().
5096
         *
5097
         * @return self<int|string,mixed> New map with a reversed copy of the elements
5098
         * @see reversed() - Underlying method with same parameters and return value but better performance
5099
         */
5100
        public function toReversed() : self
5101
        {
5102
                return $this->reversed();
8✔
5103
        }
5104

5105

5106
        /**
5107
         * Sorts the elements in a copy of the map using new keys (alias).
5108
         *
5109
         * This method is an alias for sorted(). For performance reasons, sorted() should be
5110
         * preferred because it uses one method call less than toSorted().
5111
         *
5112
         * @param int $options Sort options for PHP sort()
5113
         * @return self<int|string,mixed> New map with a sorted copy of the elements
5114
         * @see sorted() - Underlying method with same parameters and return value but better performance
5115
         */
5116
        public function toSorted( int $options = SORT_REGULAR ) : self
5117
        {
5118
                return $this->sorted( $options );
8✔
5119
        }
5120

5121

5122
        /**
5123
         * Creates a HTTP query string from the map elements.
5124
         *
5125
         * Examples:
5126
         *  Map::from( ['a' => 1, 'b' => 2] )->toUrl();
5127
         *  Map::from( ['a' => ['b' => 'abc', 'c' => 'def'], 'd' => 123] )->toUrl();
5128
         *
5129
         * Results:
5130
         *  a=1&b=2
5131
         *  a%5Bb%5D=abc&a%5Bc%5D=def&d=123
5132
         *
5133
         * @return string Parameter string for GET requests
5134
         */
5135
        public function toUrl() : string
5136
        {
5137
                return http_build_query( $this->list(), '', '&', PHP_QUERY_RFC3986 );
16✔
5138
        }
5139

5140

5141
        /**
5142
         * Creates new key/value pairs using the passed function and returns a new map for the result.
5143
         *
5144
         * Examples:
5145
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5146
         *      return [$key . '-2' => $value * 2];
5147
         *  } );
5148
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5149
         *      return [$key => $value * 2, $key . $key => $value * 4];
5150
         *  } );
5151
         *  Map::from( ['a' => 2, 'b' => 4] )->transform( function( $value, $key ) {
5152
         *      return $key < 'b' ? [$key => $value * 2] : null;
5153
         *  } );
5154
         *  Map::from( ['la' => 2, 'le' => 4, 'li' => 6] )->transform( function( $value, $key ) {
5155
         *      return [$key[0] => $value * 2];
5156
         *  } );
5157
         *
5158
         * Results:
5159
         *  ['a-2' => 4, 'b-2' => 8]
5160
         *  ['a' => 4, 'aa' => 8, 'b' => 8, 'bb' => 16]
5161
         *  ['a' => 4]
5162
         *  ['l' => 12]
5163
         *
5164
         * If a key is returned twice, the last value will overwrite previous values.
5165
         *
5166
         * @param \Closure $callback Function with (value, key) parameters and returns an array of new key/value pair(s)
5167
         * @return self<int|string,mixed> New map with the new key/value pairs
5168
         * @see map() - Maps new values to the existing keys using the passed function and returns a new map for the result
5169
         * @see rekey() - Changes the keys according to the passed function
5170
         */
5171
        public function transform( \Closure $callback ) : self
5172
        {
5173
                $result = [];
32✔
5174

5175
                foreach( $this->list() as $key => $value )
32✔
5176
                {
5177
                        foreach( (array) $callback( $value, $key ) as $newkey => $newval ) {
32✔
5178
                                $result[$newkey] = $newval;
32✔
5179
                        }
5180
                }
5181

5182
                return new static( $result );
32✔
5183
        }
5184

5185

5186
        /**
5187
         * Exchanges rows and columns for a two dimensional map.
5188
         *
5189
         * Examples:
5190
         *  Map::from( [
5191
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5192
         *    ['name' => 'B', 2020 => 300, 2021 => 200, 2022 => 100],
5193
         *    ['name' => 'C', 2020 => 400, 2021 => 300, 2022 => 200],
5194
         *  ] )->transpose();
5195
         *
5196
         *  Map::from( [
5197
         *    ['name' => 'A', 2020 => 200, 2021 => 100, 2022 => 50],
5198
         *    ['name' => 'B', 2020 => 300, 2021 => 200],
5199
         *    ['name' => 'C', 2020 => 400]
5200
         *  ] );
5201
         *
5202
         * Results:
5203
         *  [
5204
         *    'name' => ['A', 'B', 'C'],
5205
         *    2020 => [200, 300, 400],
5206
         *    2021 => [100, 200, 300],
5207
         *    2022 => [50, 100, 200]
5208
         *  ]
5209
         *
5210
         *  [
5211
         *    'name' => ['A', 'B', 'C'],
5212
         *    2020 => [200, 300, 400],
5213
         *    2021 => [100, 200],
5214
         *    2022 => [50]
5215
         *  ]
5216
         *
5217
         * @return self<int|string,mixed> New map
5218
         */
5219
        public function transpose() : self
5220
        {
5221
                $result = [];
16✔
5222

5223
                foreach( (array) $this->first( [] ) as $key => $col ) {
16✔
5224
                        $result[$key] = array_column( $this->list(), $key );
16✔
5225
                }
5226

5227
                return new static( $result );
16✔
5228
        }
5229

5230

5231
        /**
5232
         * Traverses trees of nested items passing each item to the callback.
5233
         *
5234
         * This does work for nested arrays and objects with public properties or
5235
         * objects implementing __isset() and __get() methods. To build trees
5236
         * of nested items, use the tree() method.
5237
         *
5238
         * Examples:
5239
         *   Map::from( [[
5240
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5241
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5242
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5243
         *     ]
5244
         *   ]] )->traverse();
5245
         *
5246
         *   Map::from( [[
5247
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5248
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5249
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5250
         *     ]
5251
         *   ]] )->traverse( function( $entry, $key, $level, $parent ) {
5252
         *     return str_repeat( '-', $level ) . '- ' . $entry['name'];
5253
         *   } );
5254
         *
5255
         *   Map::from( [[
5256
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [
5257
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5258
         *       ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []]
5259
         *     ]
5260
         *   ]] )->traverse( function( &$entry, $key, $level, $parent ) {
5261
         *     $entry['path'] = isset( $parent['path'] ) ? $parent['path'] . '/' . $entry['name'] : $entry['name'];
5262
         *     return $entry;
5263
         *   } );
5264
         *
5265
         *   Map::from( [[
5266
         *     'id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [
5267
         *       ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []]
5268
         *     ]
5269
         *   ]] )->traverse( null, 'nodes' );
5270
         *
5271
         * Results:
5272
         *   [
5273
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...]],
5274
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => []],
5275
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => []],
5276
         *   ]
5277
         *
5278
         *   ['- n1', '-- n2', '-- n3']
5279
         *
5280
         *   [
5281
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'children' => [...], 'path' => 'n1'],
5282
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'children' => [], 'path' => 'n1/n2'],
5283
         *     ['id' => 3, 'pid' => 1, 'name' => 'n3', 'children' => [], 'path' => 'n1/n3'],
5284
         *   ]
5285
         *
5286
         *   [
5287
         *     ['id' => 1, 'pid' => null, 'name' => 'n1', 'nodes' => [...]],
5288
         *     ['id' => 2, 'pid' => 1, 'name' => 'n2', 'nodes' => []],
5289
         *   ]
5290
         *
5291
         * @param \Closure|null $callback Callback with (entry, key, level, $parent) arguments, returns the entry added to result
5292
         * @param string $nestKey Key to the children of each item
5293
         * @return self<int|string,mixed> New map with all items as flat list
5294
         */
5295
        public function traverse( ?\Closure $callback = null, string $nestKey = 'children' ) : self
5296
        {
5297
                $result = [];
40✔
5298
                $this->visit( $this->list(), $result, 0, $callback, $nestKey );
40✔
5299

5300
                return map( $result );
40✔
5301
        }
5302

5303

5304
        /**
5305
         * Creates a tree structure from the list items.
5306
         *
5307
         * Use this method to rebuild trees e.g. from database records. To traverse
5308
         * trees, use the traverse() method.
5309
         *
5310
         * Examples:
5311
         *  Map::from( [
5312
         *    ['id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1'],
5313
         *    ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2'],
5314
         *    ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3'],
5315
         *    ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4'],
5316
         *    ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5'],
5317
         *    ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6'],
5318
         *  ] )->tree( 'id', 'pid' );
5319
         *
5320
         * Results:
5321
         *   [1 => [
5322
         *     'id' => 1, 'pid' => null, 'lvl' => 0, 'name' => 'n1', 'children' => [
5323
         *       2 => ['id' => 2, 'pid' => 1, 'lvl' => 1, 'name' => 'n2', 'children' => [
5324
         *         3 => ['id' => 3, 'pid' => 2, 'lvl' => 2, 'name' => 'n3', 'children' => []]
5325
         *       ]],
5326
         *       4 => ['id' => 4, 'pid' => 1, 'lvl' => 1, 'name' => 'n4', 'children' => [
5327
         *         5 => ['id' => 5, 'pid' => 3, 'lvl' => 2, 'name' => 'n5', 'children' => []]
5328
         *       ]],
5329
         *       6 => ['id' => 6, 'pid' => 1, 'lvl' => 1, 'name' => 'n6', 'children' => []]
5330
         *     ]
5331
         *   ]]
5332
         *
5333
         * To build the tree correctly, the items must be in order or at least the
5334
         * nodes of the lower levels must come first. For a tree like this:
5335
         * n1
5336
         * |- n2
5337
         * |  |- n3
5338
         * |- n4
5339
         * |  |- n5
5340
         * |- n6
5341
         *
5342
         * Accepted item order:
5343
         * - in order: n1, n2, n3, n4, n5, n6
5344
         * - lower levels first: n1, n2, n4, n6, n3, n5
5345
         *
5346
         * If your items are unordered, apply usort() first to the map entries, e.g.
5347
         *   Map::from( [['id' => 3, 'lvl' => 2], ...] )->usort( function( $item1, $item2 ) {
5348
         *     return $item1['lvl'] <=> $item2['lvl'];
5349
         *   } );
5350
         *
5351
         * @param string $idKey Name of the key with the unique ID of the node
5352
         * @param string $parentKey Name of the key with the ID of the parent node
5353
         * @param string $nestKey Name of the key with will contain the children of the node
5354
         * @return self<int|string,mixed> New map with one or more root tree nodes
5355
         */
5356
        public function tree( string $idKey, string $parentKey, string $nestKey = 'children' ) : self
5357
        {
5358
                $this->list();
8✔
5359
                $trees = $refs = [];
8✔
5360

5361
                foreach( $this->list as &$node )
8✔
5362
                {
5363
                        $node[$nestKey] = [];
8✔
5364
                        $refs[$node[$idKey]] = &$node;
8✔
5365

5366
                        if( $node[$parentKey] ) {
8✔
5367
                                $refs[$node[$parentKey]][$nestKey][$node[$idKey]] = &$node;
8✔
5368
                        } else {
5369
                                $trees[$node[$idKey]] = &$node;
8✔
5370
                        }
5371
                }
5372

5373
                return map( $trees );
8✔
5374
        }
5375

5376

5377
        /**
5378
         * Removes the passed characters from the left/right of all strings.
5379
         *
5380
         * Examples:
5381
         *  Map::from( [" abc\n", "\tcde\r\n"] )->trim();
5382
         *  Map::from( ["a b c", "cbax"] )->trim( 'abc' );
5383
         *
5384
         * Results:
5385
         * The first example will return ["abc", "cde"] while the second one will return [" b ", "x"].
5386
         *
5387
         * @param string $chars List of characters to trim
5388
         * @return self<int|string,mixed> Updated map for fluid interface
5389
         */
5390
        public function trim( string $chars = " \n\r\t\v\x00" ) : self
5391
        {
5392
                foreach( $this->list() as &$entry )
8✔
5393
                {
5394
                        if( is_string( $entry ) ) {
8✔
5395
                                $entry = trim( $entry, $chars );
8✔
5396
                        }
5397
                }
5398

5399
                return $this;
8✔
5400
        }
5401

5402

5403
        /**
5404
         * Sorts all elements using a callback and maintains the key association.
5405
         *
5406
         * The given callback will be used to compare the values. The callback must accept
5407
         * two parameters (item A and B) and must return -1 if item A is smaller than
5408
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5409
         * method name and an anonymous function can be passed.
5410
         *
5411
         * Examples:
5412
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( 'strcasecmp' );
5413
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasort( function( $itemA, $itemB ) {
5414
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5415
         *  } );
5416
         *
5417
         * Results:
5418
         *  ['b' => 'a', 'a' => 'B']
5419
         *  ['b' => 'a', 'a' => 'B']
5420
         *
5421
         * The keys are preserved using this method and no new map is created.
5422
         *
5423
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5424
         * @return self<int|string,mixed> Updated map for fluid interface
5425
         */
5426
        public function uasort( callable $callback ) : self
5427
        {
5428
                uasort( $this->list(), $callback );
16✔
5429
                return $this;
16✔
5430
        }
5431

5432

5433
        /**
5434
         * Sorts all elements using a callback and maintains the key association.
5435
         *
5436
         * The given callback will be used to compare the values. The callback must accept
5437
         * two parameters (item A and B) and must return -1 if item A is smaller than
5438
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5439
         * method name and an anonymous function can be passed.
5440
         *
5441
         * Examples:
5442
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( 'strcasecmp' );
5443
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->uasorted( function( $itemA, $itemB ) {
5444
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5445
         *  } );
5446
         *
5447
         * Results:
5448
         *  ['b' => 'a', 'a' => 'B']
5449
         *  ['b' => 'a', 'a' => 'B']
5450
         *
5451
         * The keys are preserved using this method and a new map is created.
5452
         *
5453
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5454
         * @return self<int|string,mixed> Updated map for fluid interface
5455
         */
5456
        public function uasorted( callable $callback ) : self
5457
        {
5458
                return ( clone $this )->uasort( $callback );
8✔
5459
        }
5460

5461

5462
        /**
5463
         * Sorts the map elements by their keys using a callback.
5464
         *
5465
         * The given callback will be used to compare the keys. The callback must accept
5466
         * two parameters (key A and B) and must return -1 if key A is smaller than
5467
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5468
         * method name and an anonymous function can be passed.
5469
         *
5470
         * Examples:
5471
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( 'strcasecmp' );
5472
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksort( function( $keyA, $keyB ) {
5473
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5474
         *  } );
5475
         *
5476
         * Results:
5477
         *  ['a' => 'b', 'B' => 'a']
5478
         *  ['a' => 'b', 'B' => 'a']
5479
         *
5480
         * The keys are preserved using this method and no new map is created.
5481
         *
5482
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5483
         * @return self<int|string,mixed> Updated map for fluid interface
5484
         */
5485
        public function uksort( callable $callback ) : self
5486
        {
5487
                uksort( $this->list(), $callback );
16✔
5488
                return $this;
16✔
5489
        }
5490

5491

5492
        /**
5493
         * Sorts a copy of the map elements by their keys using a callback.
5494
         *
5495
         * The given callback will be used to compare the keys. The callback must accept
5496
         * two parameters (key A and B) and must return -1 if key A is smaller than
5497
         * key B, 0 if both are equal and 1 if key A is greater than key B. Both, a
5498
         * method name and an anonymous function can be passed.
5499
         *
5500
         * Examples:
5501
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( 'strcasecmp' );
5502
         *  Map::from( ['B' => 'a', 'a' => 'b'] )->uksorted( function( $keyA, $keyB ) {
5503
         *      return strtolower( $keyA ) <=> strtolower( $keyB );
5504
         *  } );
5505
         *
5506
         * Results:
5507
         *  ['a' => 'b', 'B' => 'a']
5508
         *  ['a' => 'b', 'B' => 'a']
5509
         *
5510
         * The keys are preserved using this method and a new map is created.
5511
         *
5512
         * @param callable $callback Function with (keyA, keyB) parameters and returns -1 (<), 0 (=) and 1 (>)
5513
         * @return self<int|string,mixed> Updated map for fluid interface
5514
         */
5515
        public function uksorted( callable $callback ) : self
5516
        {
5517
                return ( clone $this )->uksort( $callback );
8✔
5518
        }
5519

5520

5521
        /**
5522
         * Builds a union of the elements and the given elements without overwriting existing ones.
5523
         * Existing keys in the map will not be overwritten
5524
         *
5525
         * Examples:
5526
         *  Map::from( [0 => 'a', 1 => 'b'] )->union( [0 => 'c'] );
5527
         *  Map::from( ['a' => 1, 'b' => 2] )->union( ['c' => 1] );
5528
         *
5529
         * Results:
5530
         * The first example will result in [0 => 'a', 1 => 'b'] because the key 0
5531
         * isn't overwritten. In the second example, the result will be a combined
5532
         * list: ['a' => 1, 'b' => 2, 'c' => 1].
5533
         *
5534
         * If list entries should be overwritten,  please use merge() instead!
5535
         * The keys are preserved using this method and no new map is created.
5536
         *
5537
         * @param iterable<int|string,mixed> $elements List of elements
5538
         * @return self<int|string,mixed> Updated map for fluid interface
5539
         */
5540
        public function union( iterable $elements ) : self
5541
        {
5542
                $this->list = $this->list() + $this->array( $elements );
16✔
5543
                return $this;
16✔
5544
        }
5545

5546

5547
        /**
5548
         * Returns only unique elements from the map incl. their keys.
5549
         *
5550
         * Examples:
5551
         *  Map::from( [0 => 'a', 1 => 'b', 2 => 'b', 3 => 'c'] )->unique();
5552
         *  Map::from( [['p' => '1'], ['p' => 1], ['p' => 2]] )->unique( 'p' )
5553
         *  Map::from( [['i' => ['p' => '1']], ['i' => ['p' => 1]]] )->unique( 'i/p' )
5554
         *
5555
         * Results:
5556
         * [0 => 'a', 1 => 'b', 3 => 'c']
5557
         * [['p' => 1], ['p' => 2]]
5558
         * [['i' => ['p' => '1']]]
5559
         *
5560
         * Two elements are considered equal if comparing their string representions returns TRUE:
5561
         * (string) $elem1 === (string) $elem2
5562
         *
5563
         * The keys of the elements are only preserved in the new map if no key is passed.
5564
         *
5565
         * @param string|null $key Key or path of the nested array or object to check for
5566
         * @return self<int|string,mixed> New map
5567
         */
5568
        public function unique( ?string $key = null ) : self
5569
        {
5570
                if( $key !== null ) {
32✔
5571
                        return $this->col( null, $key )->values();
16✔
5572
                }
5573

5574
                return new static( array_unique( $this->list() ) );
16✔
5575
        }
5576

5577

5578
        /**
5579
         * Pushes an element onto the beginning of the map without returning a new map.
5580
         *
5581
         * Examples:
5582
         *  Map::from( ['a', 'b'] )->unshift( 'd' );
5583
         *  Map::from( ['a', 'b'] )->unshift( 'd', 'first' );
5584
         *
5585
         * Results:
5586
         *  ['d', 'a', 'b']
5587
         *  ['first' => 'd', 0 => 'a', 1 => 'b']
5588
         *
5589
         * The keys of the elements are only preserved in the new map if no key is passed.
5590
         *
5591
         * Performance note:
5592
         * The bigger the list, the higher the performance impact because unshift()
5593
         * needs to create a new list and copies all existing elements to the new
5594
         * array. Usually, it's better to push() new entries at the end and reverse()
5595
         * the list afterwards:
5596
         *
5597
         *  $map->push( 'a' )->push( 'b' )->reverse();
5598
         * instead of
5599
         *  $map->unshift( 'a' )->unshift( 'b' );
5600
         *
5601
         * @param mixed $value Item to add at the beginning
5602
         * @param int|string|null $key Key for the item or NULL to reindex all numerical keys
5603
         * @return self<int|string,mixed> Updated map for fluid interface
5604
         */
5605
        public function unshift( $value, $key = null ) : self
5606
        {
5607
                if( $key === null ) {
24✔
5608
                        array_unshift( $this->list(), $value );
16✔
5609
                } else {
5610
                        $this->list = [$key => $value] + $this->list();
8✔
5611
                }
5612

5613
                return $this;
24✔
5614
        }
5615

5616

5617
        /**
5618
         * Sorts all elements using a callback using new keys.
5619
         *
5620
         * The given callback will be used to compare the values. The callback must accept
5621
         * two parameters (item A and B) and must return -1 if item A is smaller than
5622
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5623
         * method name and an anonymous function can be passed.
5624
         *
5625
         * Examples:
5626
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( 'strcasecmp' );
5627
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usort( function( $itemA, $itemB ) {
5628
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5629
         *  } );
5630
         *
5631
         * Results:
5632
         *  [0 => 'a', 1 => 'B']
5633
         *  [0 => 'a', 1 => 'B']
5634
         *
5635
         * The keys aren't preserved and elements get a new index. No new map is created.
5636
         *
5637
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5638
         * @return self<int|string,mixed> Updated map for fluid interface
5639
         */
5640
        public function usort( callable $callback ) : self
5641
        {
5642
                usort( $this->list(), $callback );
16✔
5643
                return $this;
16✔
5644
        }
5645

5646

5647
        /**
5648
         * Sorts a copy of all elements using a callback using new keys.
5649
         *
5650
         * The given callback will be used to compare the values. The callback must accept
5651
         * two parameters (item A and B) and must return -1 if item A is smaller than
5652
         * item B, 0 if both are equal and 1 if item A is greater than item B. Both, a
5653
         * method name and an anonymous function can be passed.
5654
         *
5655
         * Examples:
5656
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( 'strcasecmp' );
5657
         *  Map::from( ['a' => 'B', 'b' => 'a'] )->usorted( function( $itemA, $itemB ) {
5658
         *      return strtolower( $itemA ) <=> strtolower( $itemB );
5659
         *  } );
5660
         *
5661
         * Results:
5662
         *  [0 => 'a', 1 => 'B']
5663
         *  [0 => 'a', 1 => 'B']
5664
         *
5665
         * The keys aren't preserved, elements get a new index and a new map is created.
5666
         *
5667
         * @param callable $callback Function with (itemA, itemB) parameters and returns -1 (<), 0 (=) and 1 (>)
5668
         * @return self<int|string,mixed> Updated map for fluid interface
5669
         */
5670
        public function usorted( callable $callback ) : self
5671
        {
5672
                return ( clone $this )->usort( $callback );
8✔
5673
        }
5674

5675

5676
        /**
5677
         * Resets the keys and return the values in a new map.
5678
         *
5679
         * Examples:
5680
         *  Map::from( ['x' => 'b', 2 => 'a', 'c'] )->values();
5681
         *
5682
         * Results:
5683
         * A new map with [0 => 'b', 1 => 'a', 2 => 'c'] as content
5684
         *
5685
         * @return self<int|string,mixed> New map of the values
5686
         */
5687
        public function values() : self
5688
        {
5689
                return new static( array_values( $this->list() ) );
96✔
5690
        }
5691

5692

5693
        /**
5694
         * Applies the given callback to all elements.
5695
         *
5696
         * To change the values of the Map, specify the value parameter as reference
5697
         * (&$value). You can only change the values but not the keys nor the array
5698
         * structure.
5699
         *
5700
         * Examples:
5701
         *  Map::from( ['a', 'B', ['c', 'd'], 'e'] )->walk( function( &$value ) {
5702
         *    $value = strtoupper( $value );
5703
         *  } );
5704
         *  Map::from( [66 => 'B', 97 => 'a'] )->walk( function( $value, $key ) {
5705
         *    echo 'ASCII ' . $key . ' is ' . $value . "\n";
5706
         *  } );
5707
         *  Map::from( [1, 2, 3] )->walk( function( &$value, $key, $data ) {
5708
         *    $value = $data[$value] ?? $value;
5709
         *  }, [1 => 'one', 2 => 'two'] );
5710
         *
5711
         * Results:
5712
         * The first example will change the Map elements to:
5713
         *   ['A', 'B', ['C', 'D'], 'E']
5714
         * The output of the second one will be:
5715
         *  ASCII 66 is B
5716
         *  ASCII 97 is a
5717
         * The last example changes the Map elements to:
5718
         *  ['one', 'two', 3]
5719
         *
5720
         * By default, Map elements which are arrays will be traversed recursively.
5721
         * To iterate over the Map elements only, pass FALSE as third parameter.
5722
         *
5723
         * @param callable $callback Function with (item, key, data) parameters
5724
         * @param mixed $data Arbitrary data that will be passed to the callback as third parameter
5725
         * @param bool $recursive TRUE to traverse sub-arrays recursively (default), FALSE to iterate Map elements only
5726
         * @return self<int|string,mixed> Updated map for fluid interface
5727
         */
5728
        public function walk( callable $callback, $data = null, bool $recursive = true ) : self
5729
        {
5730
                if( $recursive ) {
24✔
5731
                        array_walk_recursive( $this->list(), $callback, $data );
16✔
5732
                } else {
5733
                        array_walk( $this->list(), $callback, $data );
8✔
5734
                }
5735

5736
                return $this;
24✔
5737
        }
5738

5739

5740
        /**
5741
         * Filters the list of elements by a given condition.
5742
         *
5743
         * Examples:
5744
         *  Map::from( [
5745
         *    ['id' => 1, 'type' => 'name'],
5746
         *    ['id' => 2, 'type' => 'short'],
5747
         *  ] )->where( 'type', '==', 'name' );
5748
         *
5749
         *  Map::from( [
5750
         *    ['id' => 3, 'price' => 10],
5751
         *    ['id' => 4, 'price' => 50],
5752
         *  ] )->where( 'price', '>', 20 );
5753
         *
5754
         *  Map::from( [
5755
         *    ['id' => 3, 'price' => 10],
5756
         *    ['id' => 4, 'price' => 50],
5757
         *  ] )->where( 'price', 'in', [10, 25] );
5758
         *
5759
         *  Map::from( [
5760
         *    ['id' => 3, 'price' => 10],
5761
         *    ['id' => 4, 'price' => 50],
5762
         *  ] )->where( 'price', '-', [10, 100] );
5763
         *
5764
         *  Map::from( [
5765
         *    ['item' => ['id' => 3, 'price' => 10]],
5766
         *    ['item' => ['id' => 4, 'price' => 50]],
5767
         *  ] )->where( 'item/price', '>', 30 );
5768
         *
5769
         * Results:
5770
         *  [0 => ['id' => 1, 'type' => 'name']]
5771
         *  [1 => ['id' => 4, 'price' => 50]]
5772
         *  [0 => ['id' => 3, 'price' => 10]]
5773
         *  [0 => ['id' => 3, 'price' => 10], ['id' => 4, 'price' => 50]]
5774
         *  [1 => ['item' => ['id' => 4, 'price' => 50]]]
5775
         *
5776
         * Available operators are:
5777
         * * '==' : Equal
5778
         * * '===' : Equal and same type
5779
         * * '!=' : Not equal
5780
         * * '!==' : Not equal and same type
5781
         * * '<=' : Smaller than an equal
5782
         * * '>=' : Greater than an equal
5783
         * * '<' : Smaller
5784
         * * '>' : Greater
5785
         * 'in' : Array of value which are in the list of values
5786
         * '-' : Values between array of start and end value, e.g. [10, 100] (inclusive)
5787
         *
5788
         * This does also work for multi-dimensional arrays by passing the keys
5789
         * of the arrays separated by the delimiter ("/" by default), e.g. "key1/key2/key3"
5790
         * to get "val" from ['key1' => ['key2' => ['key3' => 'val']]]. The same applies to
5791
         * public properties of objects or objects implementing __isset() and __get() methods.
5792
         *
5793
         * The keys of the original map are preserved in the returned map.
5794
         *
5795
         * @param string $key Key or path of the value in the array or object used for comparison
5796
         * @param string $op Operator used for comparison
5797
         * @param mixed $value Value used for comparison
5798
         * @return self<int|string,mixed> New map for fluid interface
5799
         */
5800
        public function where( string $key, string $op, $value ) : self
5801
        {
5802
                return $this->filter( function( $item ) use ( $key, $op, $value ) {
36✔
5803

5804
                        if( ( $val = $this->val( $item, explode( $this->sep, $key ) ) ) !== null )
48✔
5805
                        {
5806
                                switch( $op )
5✔
5807
                                {
5808
                                        case '-':
40✔
5809
                                                $list = (array) $value;
8✔
5810
                                                return $val >= current( $list ) && $val <= end( $list );
8✔
5811
                                        case 'in': return in_array( $val, (array) $value );
32✔
5812
                                        case '<': return $val < $value;
24✔
5813
                                        case '>': return $val > $value;
24✔
5814
                                        case '<=': return $val <= $value;
16✔
5815
                                        case '>=': return $val >= $value;
16✔
5816
                                        case '===': return $val === $value;
16✔
5817
                                        case '!==': return $val !== $value;
16✔
5818
                                        case '!=': return $val != $value;
16✔
5819
                                        default: return $val == $value;
16✔
5820
                                }
5821
                        }
5822

5823
                        return false;
8✔
5824
                } );
48✔
5825
        }
5826

5827

5828
        /**
5829
         * Returns a copy of the map with the element at the given index replaced with the given value.
5830
         *
5831
         * Examples:
5832
         *  $m = Map::from( ['a' => 1] );
5833
         *  $m->with( 2, 'b' );
5834
         *  $m->with( 'a', 2 );
5835
         *
5836
         * Results:
5837
         *  ['a' => 1, 2 => 'b']
5838
         *  ['a' => 2]
5839
         *
5840
         * The original map ($m) stays untouched!
5841
         * This method is a shortcut for calling the copy() and set() methods.
5842
         *
5843
         * @param int|string $key Array key to set or replace
5844
         * @param mixed $value New value for the given key
5845
         * @return self<int|string,mixed> New map
5846
         */
5847
        public function with( $key, $value ) : self
5848
        {
5849
                return ( clone $this )->set( $key, $value );
8✔
5850
        }
5851

5852

5853
        /**
5854
         * Merges the values of all arrays at the corresponding index.
5855
         *
5856
         * Examples:
5857
         *  $en = ['one', 'two', 'three'];
5858
         *  $es = ['uno', 'dos', 'tres'];
5859
         *  $m = Map::from( [1, 2, 3] )->zip( $en, $es );
5860
         *
5861
         * Results:
5862
         *  [
5863
         *    [1, 'one', 'uno'],
5864
         *    [2, 'two', 'dos'],
5865
         *    [3, 'three', 'tres'],
5866
         *  ]
5867
         *
5868
         * @param array<int|string,mixed>|\Traversable<int|string,mixed>|\Iterator<int|string,mixed> $arrays List of arrays to merge with at the same position
5869
         * @return self<int|string,mixed> New map of arrays
5870
         */
5871
        public function zip( ...$arrays ) : self
5872
        {
5873
                $args = array_map( function( $items ) {
6✔
5874
                        return $this->array( $items );
8✔
5875
                }, $arrays );
8✔
5876

5877
                return new static( array_map( null, $this->list(), ...$args ) );
8✔
5878
        }
5879

5880

5881
        /**
5882
         * Returns a plain array of the given elements.
5883
         *
5884
         * @param mixed $elements List of elements or single value
5885
         * @return array<int|string,mixed> Plain array
5886
         */
5887
        protected function array( $elements ) : array
5888
        {
5889
                if( is_array( $elements ) ) {
2,024✔
5890
                        return $elements;
1,832✔
5891
                }
5892

5893
                if( $elements instanceof \Closure ) {
424✔
5894
                        return (array) $elements();
120✔
5895
                }
5896

5897
                if( $elements instanceof \Aimeos\Map ) {
304✔
5898
                        return $elements->toArray();
184✔
5899
                }
5900

5901
                if( is_iterable( $elements ) ) {
128✔
5902
                        return iterator_to_array( $elements, true );
24✔
5903
                }
5904

5905
                return $elements !== null ? [$elements] : [];
104✔
5906
        }
5907

5908

5909
        /**
5910
         * Flattens a multi-dimensional array or map into a single level array.
5911
         *
5912
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
5913
         * @param array<mixed> &$result Will contain all elements from the multi-dimensional arrays afterwards
5914
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
5915
         */
5916
        protected function flatten( iterable $entries, array &$result, int $depth ) : void
5917
        {
5918
                foreach( $entries as $entry )
40✔
5919
                {
5920
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
5921
                                $this->flatten( $entry, $result, $depth - 1 );
32✔
5922
                        } else {
5923
                                $result[] = $entry;
40✔
5924
                        }
5925
                }
5926
        }
10✔
5927

5928

5929
        /**
5930
         * Flattens a multi-dimensional array or map into a single level array.
5931
         *
5932
         * @param iterable<int|string,mixed> $entries Single of multi-level array, map or everything foreach can be used with
5933
         * @param array<int|string,mixed> $result Will contain all elements from the multi-dimensional arrays afterwards
5934
         * @param int $depth Number of levels to flatten in multi-dimensional arrays
5935
         */
5936
        protected function kflatten( iterable $entries, array &$result, int $depth ) : void
5937
        {
5938
                foreach( $entries as $key => $entry )
40✔
5939
                {
5940
                        if( is_iterable( $entry ) && $depth > 0 ) {
40✔
5941
                                $this->kflatten( $entry, $result, $depth - 1 );
40✔
5942
                        } else {
5943
                                $result[$key] = $entry;
40✔
5944
                        }
5945
                }
5946
        }
10✔
5947

5948

5949
        /**
5950
         * Returns a reference to the array of elements
5951
         *
5952
         * @return array Reference to the array of elements
5953
         */
5954
        protected function &list() : array
5955
        {
5956
                if( !is_array( $this->list ) ) {
2,720✔
5957
                        $this->list = $this->array( $this->list );
×
5958
                }
5959

5960
                return $this->list;
2,720✔
5961
        }
5962

5963

5964
        /**
5965
         * Returns a configuration value from an array.
5966
         *
5967
         * @param array<mixed>|object $entry The array or object to look at
5968
         * @param array<string> $parts Path parts to look for inside the array or object
5969
         * @return mixed Found value or null if no value is available
5970
         */
5971
        protected function val( $entry, array $parts )
5972
        {
5973
                foreach( $parts as $part )
312✔
5974
                {
5975
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$part] ) ) {
312✔
5976
                                $entry = $entry[$part];
176✔
5977
                        } elseif( is_object( $entry ) && isset( $entry->{$part} ) ) {
200✔
5978
                                $entry = $entry->{$part};
8✔
5979
                        } else {
5980
                                return null;
207✔
5981
                        }
5982
                }
5983

5984
                return $entry;
176✔
5985
        }
5986

5987

5988
        /**
5989
         * Visits each entry, calls the callback and returns the items in the result argument
5990
         *
5991
         * @param iterable<int|string,mixed> $entries List of entries with children (optional)
5992
         * @param array<mixed> $result Numerically indexed list of all visited entries
5993
         * @param int $level Current depth of the nodes in the tree
5994
         * @param \Closure|null $callback Callback with ($entry, $key, $level) arguments, returns the entry added to result
5995
         * @param string $nestKey Key to the children of each entry
5996
         * @param array<mixed>|object|null $parent Parent entry
5997
         */
5998
        protected function visit( iterable $entries, array &$result, int $level, ?\Closure $callback, string $nestKey, $parent = null ) : void
5999
        {
6000
                foreach( $entries as $key => $entry )
40✔
6001
                {
6002
                        $result[] = $callback ? $callback( $entry, $key, $level, $parent ) : $entry;
40✔
6003

6004
                        if( ( is_array( $entry ) || $entry instanceof \ArrayAccess ) && isset( $entry[$nestKey] ) ) {
40✔
6005
                                $this->visit( $entry[$nestKey], $result, $level + 1, $callback, $nestKey, $entry );
32✔
6006
                        } elseif( is_object( $entry ) && isset( $entry->{$nestKey} ) ) {
8✔
6007
                                $this->visit( $entry->{$nestKey}, $result, $level + 1, $callback, $nestKey, $entry );
12✔
6008
                        }
6009
                }
6010
        }
10✔
6011
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc