Passed
Push — master ( 91612e...bbf21d )
by Jean
01:47
created

Arrays   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 645
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 98
eloc 235
dl 0
loc 645
rs 2
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A unique() 0 23 4
A isAssociative() 0 8 1
B mergeRecursiveCustom() 0 55 9
A mergePreservingDistincts() 0 31 4
A mustBeCountable() 0 29 4
B sum() 0 30 7
A mustBeTraversable() 0 29 4
B weightedMean() 0 36 9
A keyExists() 0 17 4
A isCountable() 0 3 2
A cleanMergeBuckets() 0 18 5
A cleanMergeDuplicates() 0 22 6
C merge() 0 65 17
A isTraversable() 0 3 2
F generateGroupId() 0 101 20

How to fix   Complexity   

Complex Class

Complex classes like Arrays often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Arrays, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace JClaveau\Arrays;
3
use       JClaveau\Exceptions\UsageException;
4
5
/**
6
 *
7
 */
8
class Arrays
9
{
10
    /**
11
     * Taken from Kohana's Arr class.
12
     *
13
     * Tests if an array is associative or not.
14
     *
15
     *     // Returns TRUE
16
     *     Arr::isAssoc(array('username' => 'john.doe'));
17
     *
18
     *     // Returns FALSE
19
     *     Arr::isAssoc('foo', 'bar');
20
     *
21
     * @param   array   $array  array to check
22
     * @return  boolean
23
     */
24
    public static function isAssociative(array $array)
25
    {
26
        // Keys of the array
27
        $keys = array_keys($array);
28
29
        // If the array keys of the keys match the keys, then the array must
30
        // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
31
        return array_keys($keys) !== $keys;
32
    }
33
34
    /**
35
     * Taken from Kohana's Arr class.
36
     *
37
     * Recursively merge two or more arrays. Values in an associative array
38
     * overwrite previous values with the same key. Values in an indexed array
39
     * are appended, but only when they do not already exist in the result.
40
     *
41
     * Note that this does not work the same as [array_merge_recursive](http://php.net/array_merge_recursive)!
42
     *
43
     *     $john = array('name' => 'john', 'children' => array('fred', 'paul', 'sally', 'jane'));
44
     *     $mary = array('name' => 'mary', 'children' => array('jane'));
45
     *
46
     *     // John and Mary are married, merge them together
47
     *     $john = Arr::merge($john, $mary);
48
     *
49
     *     // The output of $john will now be:
50
     *     array('name' => 'mary', 'children' => array('fred', 'paul', 'sally', 'jane'))
51
     *
52
     * @param   array  $array1      initial array
53
     * @param   array  $array2,...  array to merge
54
     * @return  array
55
     */
56
    public static function merge($array1, $array2)
57
    {
58
        if (self::isAssociative($array2))
59
        {
60
            foreach ($array2 as $key => $value)
61
            {
62
                if (is_array($value)
63
                    AND isset($array1[$key])
64
                    AND is_array($array1[$key])
65
                )
66
                {
67
                    $array1[$key] = self::merge($array1[$key], $value);
68
                }
69
                else
70
                {
71
                    $array1[$key] = $value;
72
                }
73
            }
74
        }
75
        else
76
        {
77
            foreach ($array2 as $value)
78
            {
79
                if ( ! in_array($value, $array1, TRUE))
80
                {
81
                    $array1[] = $value;
82
                }
83
            }
84
        }
85
86
        if (func_num_args() > 2)
87
        {
88
            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...
89
            {
90
                if (self::isAssociative($array2))
91
                {
92
                    foreach ($array2 as $key => $value)
93
                    {
94
                        if (is_array($value)
95
                            AND isset($array1[$key])
96
                            AND is_array($array1[$key])
97
                        )
98
                        {
99
                            $array1[$key] = self::merge($array1[$key], $value);
100
                        }
101
                        else
102
                        {
103
                            $array1[$key] = $value;
104
                        }
105
                    }
106
                }
107
                else
108
                {
109
                    foreach ($array2 as $value)
110
                    {
111
                        if ( ! in_array($value, $array1, TRUE))
112
                        {
113
                            $array1[] = $value;
114
                        }
115
                    }
116
                }
117
            }
118
        }
119
120
        return $array1;
121
    }
122
123
    /**
124
     * Equivalent of array_merge_recursive with more options.
125
     *
126
     * @param array         $existing_row
127
     * @param array         $conflict_row
128
     * @param callable|null $merge_resolver
129
     * @param int           $max_depth
130
     *
131
     * + If exist only in conflict row => add
132
     * + If same continue
133
     * + If different merge as array
134
     */
135
    public static function mergeRecursiveCustom(
136
        $existing_row,
137
        $conflict_row,
138
        callable $merge_resolver=null,
139
        $max_depth=null
140
    ){
141
        static::mustBeCountable($existing_row);
142
        static::mustBeCountable($conflict_row);
143
144
        foreach ($conflict_row as $column => $conflict_value) {
145
146
            // not existing in first array
147
            if (!isset($existing_row[$column])) {
148
                $existing_row[$column] = $conflict_value;
149
                continue;
150
            }
151
152
            $existing_value = $existing_row[$column];
153
154
            // two arrays so we recurse
155
            if (is_array($existing_value) && is_array($conflict_value)) {
156
157
                if ($max_depth === null || $max_depth > 0) {
158
                    $existing_row[$column] = static::mergeRecursiveCustom(
159
                        $existing_value,
160
                        $conflict_value,
161
                        $merge_resolver,
162
                        $max_depth - 1
163
                    );
164
                    continue;
165
                }
166
            }
167
168
            if ($merge_resolver) {
169
                $existing_row[$column] = call_user_func_array(
170
                    $merge_resolver,
171
                    [
172
                        $existing_value,
173
                        $conflict_value,
174
                        $column,
175
                    ]
176
                );
177
            }
178
            else {
179
                // same resolution as array_merge_recursive
180
                if (!is_array($existing_value)) {
181
                    $existing_row[$column] = [$existing_value];
182
                }
183
184
                // We store the new value with their previous ones
185
                $existing_row[$column][] = $conflict_value;
186
            }
187
        }
188
189
        return $existing_row;
190
    }
191
192
    /**
193
     * Merges two rows
194
     *
195
     * @param  array $existing_row
196
     * @param  array $conflict_row
197
     *
198
     * @return array
199
     */
200
    public static function mergePreservingDistincts(
201
        $existing_row,
202
        $conflict_row
203
    ){
204
        static::mustBeCountable($existing_row);
205
        static::mustBeCountable($conflict_row);
206
207
        $merge = static::mergeRecursiveCustom(
208
            $existing_row,
209
            $conflict_row,
210
            function ($existing_value, $conflict_value, $column) {
211
212
                if ( ! $existing_value instanceof MergeBucket) {
213
                    $existing_value = MergeBucket::from()->push($existing_value);
214
                }
215
216
                // We store the new value with their previous ones
217
                if ( ! $conflict_value instanceof MergeBucket) {
218
                    $conflict_value = MergeBucket::from()->push($conflict_value);
219
                }
220
221
                foreach ($conflict_value->toArray() as $conflict_key => $conflict_entry) {
222
                    $existing_value->push($conflict_entry);
223
                }
224
225
                return $existing_value;
226
            },
227
            0
228
        );
229
230
        return $merge;
231
    }
232
233
    /**
234
     * This is the cleaning part of self::mergePreservingDistincts()
235
     *
236
     * @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...
237
     * @param  array             $options : 'excluded_columns'
238
     */
239
    public static function cleanMergeDuplicates($row, array $options=[])
240
    {
241
        static::mustBeCountable($row);
242
243
        $excluded_columns = isset($options['excluded_columns'])
244
                          ? $options['excluded_columns']
245
                          : []
246
                          ;
247
248
        foreach ($row as $column => &$values) {
249
            if ( ! $values instanceof MergeBucket)
250
                continue;
251
252
            if (in_array($column, $excluded_columns))
253
                continue;
254
255
            $values = Arrays::unique($values);
256
            if (count($values) == 1)
257
                $values = $values[0];
258
        }
259
260
        return $row;
261
    }
262
263
    /**
264
     * This is the cleaning last part of self::mergePreservingDistincts()
265
     *
266
     * @param  array|Countable   $row
267
     * @param  array             $options : 'excluded_columns'
268
     *
269
     * @see mergePreservingDistincts()
270
     * @see cleanMergeDuplicates()
271
     */
272
    public static function cleanMergeBuckets($row, array $options=[])
273
    {
274
        static::mustBeCountable($row);
275
276
        $excluded_columns = isset($options['excluded_columns'])
277
                          ? $options['excluded_columns']
278
                          : []
279
                          ;
280
281
        foreach ($row as $column => &$values) {
282
            if (in_array($column, $excluded_columns))
283
                continue;
284
285
            if ($values instanceof MergeBucket)
286
                $values = $values->toArray();
287
        }
288
289
        return $row;
290
    }
291
292
    /**
293
     * Replacement of array_unique, keeping the first key.
294
     *
295
     * @param  array|\Traversable $array
296
     * @return array|\Traversable With unique values
297
     *
298
     * @todo   Options to keep another key than the first one?
299
     */
300
    public static function unique($array)
301
    {
302
        static::mustBeCountable($array);
303
304
        $ids = [];
305
        foreach ($array as $key => $value) {
306
            if (is_scalar($value)) {
307
                $id = $value;
308
            }
309
            else {
310
                $id = serialize($value);
311
            }
312
313
            if (isset($ids[ $id ])) {
314
                unset($array[ $key ]);
315
                $ids[ $id ][] = $key;
316
                continue;
317
            }
318
319
            $ids[ $id ] = [$key];
320
        }
321
322
        return $array;
323
    }
324
325
    /**
326
     */
327
    public static function keyExists($key, $array)
328
    {
329
        static::mustBeTraversable($array);
330
331
        if (is_array($array)) {
332
            return array_key_exists($key, $array);
333
        }
334
        elseif ($array instanceof ChainableArray || method_exists($array, 'keyExists')) {
335
            return $array->keyExists($key);
336
        }
337
        else {
338
            throw new \InvalidArgumentException(
339
                "keyExists() method missing on :\n". var_export($array, true)
340
            );
341
        }
342
343
        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...
344
    }
345
346
    /**
347
     * Replacement of array_sum wich throws exceptions instead of skipping
348
     * bad operands.
349
     *
350
     * @param  array|\Traversable $array
351
     * @return int|double         The sum
352
     *
353
     * @todo   Support options like 'strict', 'skip_non_scalars', 'native'
354
     */
355
    public static function sum($array)
356
    {
357
        static::mustBeCountable($array);
358
359
        $sum = 0;
360
        foreach ($array as $key => &$value) { // &for optimization
361
            if (is_scalar($value)) {
362
                $sum += $value;
363
            }
364
            elseif (is_null($value)) {
365
                continue;
366
            }
367
            elseif (is_array($value)) {
368
                throw new \InvalidArgumentException(
369
                    "Trying to sum an array with '$sum': ".var_export($value, true)
370
                );
371
            }
372
            elseif (is_object($value)) {
373
                if ( ! method_exists($value, 'toNumber')) {
374
                    throw new \InvalidArgumentEXception(
375
                         "Trying to sum a ".get_class($value)." object which cannot be casted as a number. "
376
                        ."Please add a toNumber() method."
377
                    );
378
                }
379
380
                $sum += $value->toNumber();
381
            }
382
        }
383
384
        return $sum;
385
    }
386
387
    /**
388
     * This method returns a classical mathemartic weighted mean.
389
     *
390
     * @todo It would ideally handled by a bridge with this fantastic math
391
     * lib https://github.com/markrogoyski/math-php/ but we need the support
392
     * of PHP 7 first.
393
     *
394
     * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
395
     * @see https://github.com/markrogoyski/math-php/
396
     */
397
    public static function weightedMean($values, $weights)
398
    {
399
        if ($values instanceof ChainableArray)
400
            $values = $values->toArray();
401
402
        if ($weights instanceof ChainableArray)
403
            $weights = $weights->toArray();
404
405
        if ( ! is_array($values))
406
            $values = [$values];
407
408
        if ( ! is_array($weights))
409
            $weights = [$weights];
410
411
        if (count($values) != count($weights)) {
412
            throw new \InvalidArgumentException(
413
                "Different number of "
414
                ." values and weights for weight mean calculation: \n"
415
                .var_export($values,  true)."\n\n"
416
                .var_export($weights, true)
417
            );
418
        }
419
420
        if (!$values)
421
            return null;
422
423
        $weights_sum  = array_sum($weights);
424
        if (!$weights_sum)
425
            return 0;
426
427
        $weighted_sum = 0;
428
        foreach ($values as $i => $value) {
429
            $weighted_sum += $value * $weights[$i];
430
        }
431
432
        return $weighted_sum / $weights_sum;
433
    }
434
435
    /**
436
     * This is not required anymore with PHP 7.
437
     *
438
     * @return bool
439
     */
440
    public static function isTraversable($value)
441
    {
442
        return $value instanceof \Traversable || is_array($value);
443
    }
444
445
    /**
446
     * This is not required anymore with PHP 7.
447
     *
448
     * @return bool
449
     */
450
    public static function isCountable($value)
451
    {
452
        return $value instanceof \Countable || is_array($value);
453
    }
454
455
    /**
456
     * @param  mixed $value
457
     * @return bool  Is the $value countable or not
458
     * @throws InvalidArgumentException
459
     *
460
     * @todo   NotCountableException
461
     */
462
    public static function mustBeCountable($value)
463
    {
464
        if (static::isCountable($value))
465
            return true;
466
467
        $exception = new \InvalidArgumentException(
468
            "A value must be Countable instead of: \n"
469
            .var_export($value, true)
470
        );
471
472
        // The true location of the throw is still available through the backtrace
473
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
474
        $reflectionClass = new \ReflectionClass( get_class($exception) );
475
476
        // file
477
        if (isset($trace_location['file'])) {
478
            $reflectionProperty = $reflectionClass->getProperty('file');
479
            $reflectionProperty->setAccessible(true);
480
            $reflectionProperty->setValue($exception, $trace_location['file']);
481
        }
482
483
        // line
484
        if (isset($trace_location['line'])) {
485
            $reflectionProperty = $reflectionClass->getProperty('line');
486
            $reflectionProperty->setAccessible(true);
487
            $reflectionProperty->setValue($exception, $trace_location['line']);
488
        }
489
490
        throw $exception;
491
    }
492
493
    /**
494
     * @param  mixed $value
495
     * @return bool  Is the $value traversable or not
496
     * @throws InvalidArgumentException
497
     *
498
     * @todo   NotTraversableException
499
     */
500
    public static function mustBeTraversable($value)
501
    {
502
        if (static::isTraversable($value))
503
            return true;
504
505
        $exception = new \InvalidArgumentException(
506
            "A value must be Traversable instead of: \n"
507
            .var_export($value, true)
508
        );
509
510
        // The true location of the throw is still available through the backtrace
511
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
512
        $reflectionClass = new \ReflectionClass( get_class($exception) );
513
514
        // file
515
        if (isset($trace_location['file'])) {
516
            $reflectionProperty = $reflectionClass->getProperty('file');
517
            $reflectionProperty->setAccessible(true);
518
            $reflectionProperty->setValue($exception, $trace_location['file']);
519
        }
520
521
        // line
522
        if (isset($trace_location['line'])) {
523
            $reflectionProperty = $reflectionClass->getProperty('line');
524
            $reflectionProperty->setAccessible(true);
525
            $reflectionProperty->setValue($exception, $trace_location['line']);
526
        }
527
528
        throw $exception;
529
    }
530
531
    /**
532
     * Generates an id usable in hashes to identify a single grouped row.
533
     *
534
     * @param array $row    The row of the array to group by.
535
     * @param array $groups A list of the different groups. Groups can be
536
     *                      strings describing a column name or a callable
537
     *                      function, an array representing a callable,
538
     *                      a function or an integer representing a column.
539
     *                      If the index of the group is a string, it will
540
     *                      be used as a prefix for the group name.
541
     *                      Example:
542
     *                      [
543
     *                          'column_name',
544
     *                          'function_to_call',
545
     *                          4,  //column_number
546
     *                          'group_prefix'  => function($row){},
547
     *                          'group_prefix2' => [$object, 'method'],
548
     *                      ]
549
     *
550
     * @return string       The unique identifier of the group
551
     */
552
    public static function generateGroupId($row, array $groups_definitions, array $options=[])
553
    {
554
        Arrays::mustBeCountable($row);
555
556
        $key_value_separator = ! empty($options['key_value_separator'])
557
                             ? $options['key_value_separator']
558
                             : ':'
559
                             ;
560
561
        $groups_separator    = ! empty($options['groups_separator'])
562
                             ? $options['groups_separator']
563
                             : '-'
564
                             ;
565
566
        $group_parts = [];
567
        foreach ($groups_definitions as $group_definition_key => $group_definition_value) {
568
            $part_name = '';
569
570
            if (is_string($group_definition_key)) {
571
                $part_name .= $group_definition_key.'_';
572
            }
573
574
            if (is_string($group_definition_value)) {
575
                if (    (is_array($row)              && ! array_key_exists($group_definition_value, $row))
576
                    ||  ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value))
577
                ) {
578
                    throw new UsageException(
579
                        'Unset column for group id generation: '
580
                        .var_export($group_definition_value, true)
581
                        ."\n" . var_export($row, true)
582
                    );
583
                }
584
585
                $part_name         .= $group_definition_value;
586
                $group_result_value = $row[ $group_definition_value ];
587
            }
588
            elseif (is_int($group_definition_value)) {
589
                if (    (is_array($row)              && ! array_key_exists($group_definition_value, $row))
590
                    ||  ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value))
591
                ) {
592
                    throw new UsageException(
593
                        'Unset column for group id generation: '
594
                        .var_export($group_definition_value, true)
595
                        ."\n" . var_export($row, true)
596
                    );
597
                }
598
599
                $part_name         .= $group_definition_value ? : '0';
600
                $group_result_value = $row[ $group_definition_value ];
601
            }
602
            /* TODO check this is not just dead code * /
603
            elseif (is_callable($group_definition_value)) {
604
605
                if (is_string($group_definition_value)) {
606
                    $part_name .= $group_definition_value;
607
                }
608
                // elseif (is_function($value)) {
609
                elseif (is_object($value) && ($value instanceof \Closure)) {
610
                    $part_name .= 'unnamed-closure-'
611
                                . hash('crc32b', var_export($group_definition_value, true));
612
                }
613
                elseif (is_array($group_definition_value)) {
614
                    $part_name .= implode('::', $group_definition_value);
615
                }
616
617
                $group_result_value = call_user_func_array($group_definition_value, [
618
                    $row, &$part_name
619
                ]);
620
            }
621
            /**/
622
            else {
623
                throw new UsageException(
624
                    'Bad value provided for group id generation: '
625
                    .var_export($group_definition_value, true)
626
                    ."\n" . var_export($row, true)
627
                );
628
            }
629
630
            if (!is_null($part_name))
631
                $group_parts[ $part_name ] = $group_result_value;
632
        }
633
634
        // sort the groups by names (without it the same group could have multiple ids)
635
        ksort($group_parts);
636
637
        // bidimensional implode
638
        $out = [];
639
        foreach ($group_parts as $group_name => $group_value) {
640
            if (is_object($group_value)) {
641
                $group_value = get_class($group_value)
642
                             . '_'
643
                             . hash( 'crc32b', var_export($group_value, true) );
644
            }
645
            elseif (is_array($group_value)) {
646
                $group_value = 'array_' . hash( 'crc32b', var_export($group_value, true) );
647
            }
648
649
            $out[] = $group_name . $key_value_separator . $group_value;
650
        }
651
652
        return implode($groups_separator, $out);
653
    }
654
655
    /**/
656
}
657