Test Failed
Push — master ( 79d7ec...6a4583 )
by Jean
02:12
created

Arrays::isTraversable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace JClaveau\Arrays;
3
4
/**
5
 *
6
 */
7
class Arrays
8
{
9
    /**
10
     * Taken from Kohana's Arr class.
11
     *
12
     * Tests if an array is associative or not.
13
     *
14
     *     // Returns TRUE
15
     *     Arr::isAssoc(array('username' => 'john.doe'));
16
     *
17
     *     // Returns FALSE
18
     *     Arr::isAssoc('foo', 'bar');
19
     *
20
     * @param   array   $array  array to check
21
     * @return  boolean
22
     */
23
    public static function isAssociative(array $array)
24
    {
25
        // Keys of the array
26
        $keys = array_keys($array);
27
28
        // If the array keys of the keys match the keys, then the array must
29
        // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
30
        return array_keys($keys) !== $keys;
31
    }
32
33
    /**
34
     * Taken from Kohana's Arr class.
35
     *
36
     * Recursively merge two or more arrays. Values in an associative array
37
     * overwrite previous values with the same key. Values in an indexed array
38
     * are appended, but only when they do not already exist in the result.
39
     *
40
     * Note that this does not work the same as [array_merge_recursive](http://php.net/array_merge_recursive)!
41
     *
42
     *     $john = array('name' => 'john', 'children' => array('fred', 'paul', 'sally', 'jane'));
43
     *     $mary = array('name' => 'mary', 'children' => array('jane'));
44
     *
45
     *     // John and Mary are married, merge them together
46
     *     $john = Arr::merge($john, $mary);
47
     *
48
     *     // The output of $john will now be:
49
     *     array('name' => 'mary', 'children' => array('fred', 'paul', 'sally', 'jane'))
50
     *
51
     * @param   array  $array1      initial array
52
     * @param   array  $array2,...  array to merge
53
     * @return  array
54
     */
55
    public static function merge($array1, $array2)
56
    {
57
        if (self::isAssociative($array2))
58
        {
59
            foreach ($array2 as $key => $value)
60
            {
61
                if (is_array($value)
62
                    AND isset($array1[$key])
63
                    AND is_array($array1[$key])
64
                )
65
                {
66
                    $array1[$key] = self::merge($array1[$key], $value);
67
                }
68
                else
69
                {
70
                    $array1[$key] = $value;
71
                }
72
            }
73
        }
74
        else
75
        {
76
            foreach ($array2 as $value)
77
            {
78
                if ( ! in_array($value, $array1, TRUE))
79
                {
80
                    $array1[] = $value;
81
                }
82
            }
83
        }
84
85
        if (func_num_args() > 2)
86
        {
87
            foreach (array_slice(func_get_args(), 2) as $array2)
0 ignored issues
show
introduced by
$array2 is overwriting one of the parameters of this function.
Loading history...
88
            {
89
                if (self::isAssociative($array2))
90
                {
91
                    foreach ($array2 as $key => $value)
92
                    {
93
                        if (is_array($value)
94
                            AND isset($array1[$key])
95
                            AND is_array($array1[$key])
96
                        )
97
                        {
98
                            $array1[$key] = self::merge($array1[$key], $value);
99
                        }
100
                        else
101
                        {
102
                            $array1[$key] = $value;
103
                        }
104
                    }
105
                }
106
                else
107
                {
108
                    foreach ($array2 as $value)
109
                    {
110
                        if ( ! in_array($value, $array1, TRUE))
111
                        {
112
                            $array1[] = $value;
113
                        }
114
                    }
115
                }
116
            }
117
        }
118
119
        return $array1;
120
    }
121
122
    /**
123
     * Equivalent of array_merge_recursive with more options.
124
     *
125
     * @param array         $existing_row
126
     * @param array         $conflict_row
127
     * @param callable|null $merge_resolver
128
     * @param int           $max_depth
129
     *
130
     * + If exist only in conflict row => add
131
     * + If same continue
132
     * + If different merge as array
133
     */
134
    public static function mergeRecursiveCustom(
135
        $existing_row,
136
        $conflict_row,
137
        callable $merge_resolver=null,
138
        $max_depth=null
139
    ){
140
        static::mustBeCountable($existing_row);
141
        static::mustBeCountable($conflict_row);
142
143
        foreach ($conflict_row as $column => $conflict_value) {
144
145
            // not existing in first array
146
            if (!isset($existing_row[$column])) {
147
                $existing_row[$column] = $conflict_value;
148
                continue;
149
            }
150
151
            $existing_value = $existing_row[$column];
152
153
            // two arrays so we recurse
154
            if (is_array($existing_value) && is_array($conflict_value)) {
155
156
                if ($max_depth === null || $max_depth > 0) {
157
                    $existing_row[$column] = static::mergeRecursiveCustom(
158
                        $existing_value,
159
                        $conflict_value,
160
                        $merge_resolver,
161
                        $max_depth - 1
162
                    );
163
                    continue;
164
                }
165
            }
166
167
            if ($merge_resolver) {
168
                $existing_row[$column] = call_user_func_array(
169
                    $merge_resolver,
170
                    [
171
                        $existing_value,
172
                        $conflict_value,
173
                        $column,
174
                    ]
175
                );
176
            }
177
            else {
178
                // same resolution as array_merge_recursive
179
                if (!is_array($existing_value)) {
180
                    $existing_row[$column] = [$existing_value];
181
                }
182
183
                // We store the new value with their previous ones
184
                $existing_row[$column][] = $conflict_value;
185
            }
186
        }
187
188
        return $existing_row;
189
    }
190
191
    /**
192
     * Merges two rows
193
     *
194
     * @param  array $existing_row
195
     * @param  array $conflict_row
196
     *
197
     * @return array
198
     */
199
    public static function mergePreservingDistincts(
200
        $existing_row,
201
        $conflict_row
202
    ){
203
        static::mustBeCountable($existing_row);
204
        static::mustBeCountable($conflict_row);
205
206
        $merge = static::mergeRecursiveCustom(
207
            $existing_row,
208
            $conflict_row,
209
            function ($existing_value, $conflict_value, $column) {
210
211
                if ( ! $existing_value instanceof MergeBucket) {
212
                    $existing_value = MergeBucket::from()->push($existing_value);
213
                }
214
215
                // We store the new value with their previous ones
216
                if ( ! $conflict_value instanceof MergeBucket) {
217
                    $conflict_value = MergeBucket::from()->push($conflict_value);
218
                }
219
220
                foreach ($conflict_value->toArray() as $conflict_key => $conflict_entry) {
221
                    $existing_value->push($conflict_entry);
222
                }
223
224
                return $existing_value;
225
            },
226
            0
227
        );
228
229
        return $merge;
230
    }
231
232
    /**
233
     * This is the cleaning part of self::mergePreservingDistincts()
234
     *
235
     * @param  array|Countable   $row
0 ignored issues
show
Bug introduced by
The type JClaveau\Arrays\Countable was not found. Did you mean Countable? If so, make sure to prefix the type with \.
Loading history...
236
     * @param  array             $options : 'excluded_columns'
237
     */
238
    public static function cleanMergeDuplicates($row, array $options=[])
239
    {
240
        static::mustBeCountable($row);
241
242
        $excluded_columns = isset($options['excluded_columns'])
243
                          ? $options['excluded_columns']
244
                          : []
245
                          ;
246
247
        foreach ($row as $column => &$values) {
248
            if ( ! $values instanceof MergeBucket)
249
                continue;
250
251
            if (in_array($column, $excluded_columns))
252
                continue;
253
254
            $values = Arrays::unique($values);
255
            if (count($values) == 1)
256
                $values = $values[0];
257
        }
258
259
        return $row;
260
    }
261
262
    /**
263
     * This is the cleaning last part of self::mergePreservingDistincts()
264
     *
265
     * @param  array|Countable   $row
266
     * @param  array             $options : 'excluded_columns'
267
     *
268
     * @see mergePreservingDistincts()
269
     * @see cleanMergeDuplicates()
270
     */
271
    public static function cleanMergeBuckets($row, array $options=[])
272
    {
273
        static::mustBeCountable($row);
274
275
        $excluded_columns = isset($options['excluded_columns'])
276
                          ? $options['excluded_columns']
277
                          : []
278
                          ;
279
280
        foreach ($row as $column => &$values) {
281
            if (in_array($column, $excluded_columns))
282
                continue;
283
284
            if ($values instanceof MergeBucket)
285
                $values = $values->toArray();
286
        }
287
288
        return $row;
289
    }
290
291
    /**
292
     * Replacement of array_unique, keeping the first key.
293
     *
294
     * @param  array|\Traversable $array
295
     * @return array|\Traversable With unique values
296
     *
297
     * @todo   Options to keep another key than the first one?
298
     */
299
    public static function unique($array)
300
    {
301
        static::mustBeCountable($array);
302
303
        $ids = [];
304
        foreach ($array as $key => $value) {
305
            if (is_scalar($value)) {
306
                $id = $value;
307
            }
308
            else {
309
                $id = serialize($value);
310
            }
311
312
            if (isset($ids[ $id ])) {
313
                unset($array[ $key ]);
314
                $ids[ $id ][] = $key;
315
                continue;
316
            }
317
318
            $ids[ $id ] = [$key];
319
        }
320
321
        return $array;
322
    }
323
324
    /**
325
     */
326
    public static function keyExists($key, $array)
327
    {
328
        static::mustBeTraversable($array);
329
330
        if (is_array($array)) {
331
            return array_key_exists($key, $array);
332
        }
333
        elseif ($array instanceof ChainableArray || method_exists($array, 'keyExists')) {
334
            return $array->keyExists($key);
335
        }
336
        else {
337
            throw new \InvalidArgumentException(
338
                "keyExists() method missing on :\n". var_export($array, true)
339
            );
340
        }
341
342
        return $array;
0 ignored issues
show
Unused Code introduced by
return $array is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
343
    }
344
345
    /**
346
     * Replacement of array_sum wich throws exceptions instead of skipping
347
     * bad operands.
348
     *
349
     * @param  array|\Traversable $array
350
     * @return int|double         The sum
351
     *
352
     * @todo   Support options like 'strict', 'skip_non_scalars', 'native'
353
     */
354
    public static function sum($array)
355
    {
356
        static::mustBeCountable($array);
357
358
        $sum = 0;
359
        foreach ($array as $key => &$value) { // &for optimization
360
            if (is_scalar($value)) {
361
                $sum += $value;
362
            }
363
            elseif (is_null($value)) {
364
                continue;
365
            }
366
            elseif (is_array($value)) {
367
                throw new \InvalidArgumentException(
368
                    "Trying to sum an array with '$sum': ".var_export($value, true)
369
                );
370
            }
371
            elseif (is_object($value)) {
372
                if ( ! method_exists($value, 'toNumber')) {
373
                    throw new \InvalidArgumentEXception(
374
                         "Trying to sum a ".get_class($value)." object which cannot be casted as a number. "
375
                        ."Please add a toNumber() method."
376
                    );
377
                }
378
379
                $sum += $value->toNumber();
380
            }
381
        }
382
383
        return $sum;
384
    }
385
386
    /**
387
     * This method returns a classical mathemartic weighted mean.
388
     *
389
     * @todo It would ideally handled by a bridge with this fantastic math
390
     * lib https://github.com/markrogoyski/math-php/ but we need the support
391
     * of PHP 7 first.
392
     *
393
     * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
394
     * @see https://github.com/markrogoyski/math-php/
395
     */
396
    public static function weightedMean($values, $weights)
397
    {
398
        if ($values instanceof ChainableArray)
399
            $values = $values->toArray();
400
401
        if ($weights instanceof ChainableArray)
402
            $weights = $weights->toArray();
403
404
        if ( ! is_array($values))
405
            $values = [$values];
406
407
        if ( ! is_array($weights))
408
            $weights = [$weights];
409
410
        if (count($values) != count($weights)) {
411
            throw new \InvalidArgumentException(
412
                "Different number of "
413
                ." values and weights for weight mean calculation: \n"
414
                .var_export($values,  true)."\n\n"
415
                .var_export($weights, true)
416
            );
417
        }
418
419
        if (!$values)
420
            return null;
421
422
        $weights_sum  = array_sum($weights);
423
        if (!$weights_sum)
424
            return 0;
425
426
        $weighted_sum = 0;
427
        foreach ($values as $i => $value) {
428
            $weighted_sum += $value * $weights[$i];
429
        }
430
431
        return $weighted_sum / $weights_sum;
432
    }
433
434
    /**
435
     * This is not required anymore with PHP 7.
436
     *
437
     * @return bool
438
     */
439
    public static function isTraversable($value)
440
    {
441
        return $value instanceof \Traversable || is_array($value);
442
    }
443
444
    /**
445
     * This is not required anymore with PHP 7.
446
     *
447
     * @return bool
448
     */
449
    public static function isCountable($value)
450
    {
451
        return $value instanceof \Countable || is_array($value);
452
    }
453
454
    /**
455
     */
456
    public static function mustBeCountable($value)
457
    {
458
        if ( ! static::isCountable($value) ) {
459
            $exception = new \InvalidArgumentException(
460
                "A value must be Countable instead of: \n"
461
                .var_export($value, true)
462
            );
463
464
            // The true location of the throw is still available through the backtrace
465
            $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
466
            $reflectionClass = new \ReflectionClass( get_class($exception) );
467
468
            // file
469
            if (isset($trace_location['file'])) {
470
                $reflectionProperty = $reflectionClass->getProperty('file');
471
                $reflectionProperty->setAccessible(true);
472
                $reflectionProperty->setValue($exception, $trace_location['file']);
473
            }
474
475
            // line
476
            if (isset($trace_location['line'])) {
477
                $reflectionProperty = $reflectionClass->getProperty('line');
478
                $reflectionProperty->setAccessible(true);
479
                $reflectionProperty->setValue($exception, $trace_location['line']);
480
            }
481
482
            throw $exception;
483
        }
484
    }
485
486
    /**
487
     */
488
    public static function mustBeTraversable($value)
489
    {
490
        if ( ! static::isTraversable($value) ) {
491
            $exception = new \InvalidArgumentException(
492
                "A value must be Traversable instead of: \n"
493
                .var_export($value, true)
494
            );
495
496
            // The true location of the throw is still available through the backtrace
497
            $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
498
            $reflectionClass = new \ReflectionClass( get_class($exception) );
499
500
            // file
501
            if (isset($trace_location['file'])) {
502
                $reflectionProperty = $reflectionClass->getProperty('file');
503
                $reflectionProperty->setAccessible(true);
504
                $reflectionProperty->setValue($exception, $trace_location['file']);
505
            }
506
507
            // line
508
            if (isset($trace_location['line'])) {
509
                $reflectionProperty = $reflectionClass->getProperty('line');
510
                $reflectionProperty->setAccessible(true);
511
                $reflectionProperty->setValue($exception, $trace_location['line']);
512
            }
513
514
            throw $exception;
515
        }
516
    }
517
518
    /**/
519
}
520