Passed
Push — master ( f2bc5a...8e7423 )
by Jean
04:25
created

Arrays::cleanMergeBuckets()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 20
rs 9.6111
c 0
b 0
f 0
cc 5
nc 8
nop 2
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
     * @deprecated
201
     */
202
    public static function mergePreservingDistincts(
203
        $existing_row,
204
        $conflict_row
205
    ){
206
        return self::mergeInColumnBuckets($existing_row, $conflict_row);
207
    }
208
209
    /**
210
     * Merges two rows by replacing their column values by MergeBuckets
211
     * containing their values.
212
     *
213
     * @param  array  $existing_row
214
     * @param  array  $conflict_row
215
     * @param  scalar $key
216
     *
217
     * @return array
218
     */
219
    public static function mergeInColumnBuckets(
220
        $existing_row,
221
        $conflict_row,
222
        $existing_key=null,
223
        $conflict_key=null
224
    ) {
225
        static::mustBeCountable($existing_row);
226
        static::mustBeCountable($conflict_row);
227
        
228
        $merged_row = [];
229
        foreach ($existing_row as $existing_column => $existing_value) {
230
            if ($existing_value instanceof MergeBucket) {
231
                $merged_row[ $existing_column ] = $existing_value;
232
            }
233
            else {
234
                if (isset($existing_key)) {
235
                    $merged_row[ $existing_column ] = MergeBucket::from([
236
                        $existing_key => $existing_value
237
                    ]);
238
                }
239
                else {
240
                    $merged_row[ $existing_column ] = MergeBucket::from([
241
                        $existing_value
242
                    ]);
243
                }
244
            }
245
        }
246
        
247
        foreach ($conflict_row as $conflict_column => $conflict_value) {
248
            if (! isset($merged_row[ $conflict_column ])) {
249
                $merged_row[ $conflict_column ] = new MergeBucket;
250
            }
251
            
252
            if ($conflict_value instanceof MergeBucket) {
253
                foreach ($conflict_value as $conflict_bucket_value) {
254
                    $merged_row[ $existing_column ] = $conflict_bucket_value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $existing_column seems to be defined by a foreach iteration on line 229. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
255
                }
256
            }
257
            else {
258
                if (isset($conflict_key)) {
259
                    $merged_row[ $conflict_column ][$conflict_key] = $conflict_value;
260
                }
261
                else {
262
                    $merged_row[ $conflict_column ][] = $conflict_value;
263
                }
264
            }
265
        }
266
        
267
        return $merged_row;
268
    }
269
270
    /**
271
     * This is the cleaning part of self::mergePreservingDistincts()
272
     *
273
     * @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...
274
     * @param  array             $options : 'excluded_columns'
275
     */
276
    public static function cleanMergeDuplicates($row, array $options=[])
277
    {
278
        static::mustBeCountable($row);
279
280
        $excluded_columns = isset($options['excluded_columns'])
281
                          ? $options['excluded_columns']
282
                          : []
283
                          ;
284
285
        foreach ($row as $column => &$values) {
286
            if ( ! $values instanceof MergeBucket)
287
                continue;
288
289
            if (in_array($column, $excluded_columns))
290
                continue;
291
292
            $values = Arrays::unique($values);
293
            if (count($values) == 1)
294
                $values = $values[0];
295
        }
296
297
        return $row;
298
    }
299
300
    /**
301
     * This is the cleaning last part of self::mergePreservingDistincts()
302
     *
303
     * @param  array|Countable   $row
304
     * @param  array             $options : 'excluded_columns'
305
     *
306
     * @see mergePreservingDistincts()
307
     * @see cleanMergeDuplicates()
308
     */
309
    public static function cleanMergeBuckets($row, array $options=[])
310
    {
311
        static::mustBeCountable($row);
312
313
        $excluded_columns = isset($options['excluded_columns'])
314
                          ? $options['excluded_columns']
315
                          : []
316
                          ;
317
318
        foreach ($row as $column => &$values) {
319
            if (in_array($column, $excluded_columns)) {
320
                continue;
321
            }
322
323
            if ($values instanceof MergeBucket) {
324
                $values = $values->toArray();
325
            }
326
        }
327
328
        return $row;
329
    }
330
331
    /**
332
     * Replacement of array_unique, keeping the first key.
333
     *
334
     * @param  array|\Traversable $array
335
     * @return array|\Traversable With unique values
336
     *
337
     * @todo   Options to keep another key than the first one?
338
     */
339
    public static function unique($array)
340
    {
341
        static::mustBeCountable($array);
342
343
        $ids = [];
344
        foreach ($array as $key => $value) {
345
            if (is_scalar($value)) {
346
                $id = $value;
347
            }
348
            else {
349
                $id = serialize($value);
350
            }
351
352
            if (isset($ids[ $id ])) {
353
                unset($array[ $key ]);
354
                $ids[ $id ][] = $key;
355
                continue;
356
            }
357
358
            $ids[ $id ] = [$key];
359
        }
360
361
        return $array;
362
    }
363
364
    /**
365
     */
366
    public static function keyExists($key, $array)
367
    {
368
        static::mustBeTraversable($array);
369
370
        if (is_array($array)) {
371
            return array_key_exists($key, $array);
372
        }
373
        elseif ($array instanceof ChainableArray || method_exists($array, 'keyExists')) {
374
            return $array->keyExists($key);
375
        }
376
        else {
377
            throw new \InvalidArgumentException(
378
                "keyExists() method missing on :\n". var_export($array, true)
379
            );
380
        }
381
382
        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...
383
    }
384
385
    /**
386
     * Replacement of array_sum wich throws exceptions instead of skipping
387
     * bad operands.
388
     *
389
     * @param  array|\Traversable $array
390
     * @return int|double         The sum
391
     *
392
     * @todo   Support options like 'strict', 'skip_non_scalars', 'native'
393
     */
394
    public static function sum($array)
395
    {
396
        static::mustBeCountable($array);
397
398
        $sum = 0;
399
        foreach ($array as $key => &$value) { // &for optimization
400
            if (is_scalar($value)) {
401
                $sum += $value;
402
            }
403
            elseif (is_null($value)) {
404
                continue;
405
            }
406
            elseif (is_array($value)) {
407
                throw new \InvalidArgumentException(
408
                    "Trying to sum an array with '$sum': ".var_export($value, true)
409
                );
410
            }
411
            elseif (is_object($value)) {
412
                if ( ! method_exists($value, 'toNumber')) {
413
                    throw new \InvalidArgumentEXception(
414
                         "Trying to sum a ".get_class($value)." object which cannot be casted as a number. "
415
                        ."Please add a toNumber() method."
416
                    );
417
                }
418
419
                $sum += $value->toNumber();
420
            }
421
        }
422
423
        return $sum;
424
    }
425
426
    /**
427
     * This method returns a classical mathemartic weighted mean.
428
     *
429
     * @todo It would ideally handled by a bridge with this fantastic math
430
     * lib https://github.com/markrogoyski/math-php/ but we need the support
431
     * of PHP 7 first.
432
     *
433
     * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
434
     * @see https://github.com/markrogoyski/math-php/
435
     */
436
    public static function weightedMean($values, $weights)
437
    {
438
        if ($values instanceof ChainableArray)
439
            $values = $values->toArray();
440
441
        if ($weights instanceof ChainableArray)
442
            $weights = $weights->toArray();
443
444
        if ( ! is_array($values))
445
            $values = [$values];
446
447
        if ( ! is_array($weights))
448
            $weights = [$weights];
449
450
        if (count($values) != count($weights)) {
451
            throw new \InvalidArgumentException(
452
                "Different number of "
453
                ." values and weights for weight mean calculation: \n"
454
                .var_export($values,  true)."\n\n"
455
                .var_export($weights, true)
456
            );
457
        }
458
459
        if (!$values)
460
            return null;
461
462
        $weights_sum  = array_sum($weights);
463
        if (!$weights_sum)
464
            return 0;
465
466
        $weighted_sum = 0;
467
        foreach ($values as $i => $value) {
468
            $weighted_sum += $value * $weights[$i];
469
        }
470
471
        return $weighted_sum / $weights_sum;
472
    }
473
474
    /**
475
     * This is not required anymore with PHP 7.
476
     *
477
     * @return bool
478
     */
479
    public static function isTraversable($value)
480
    {
481
        return $value instanceof \Traversable || is_array($value);
482
    }
483
484
    /**
485
     * This is not required anymore with PHP 7.
486
     *
487
     * @return bool
488
     */
489
    public static function isCountable($value)
490
    {
491
        return $value instanceof \Countable || is_array($value);
492
    }
493
494
    /**
495
     * @param  mixed $value
496
     * @return bool  Is the $value countable or not
497
     * @throws InvalidArgumentException
498
     *
499
     * @todo   NotCountableException
500
     */
501
    public static function mustBeCountable($value)
502
    {
503
        if (static::isCountable($value))
504
            return true;
505
506
        $exception = new \InvalidArgumentException(
507
            "A value must be Countable instead of: \n"
508
            .var_export($value, true)
509
        );
510
511
        // The true location of the throw is still available through the backtrace
512
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
513
        $reflectionClass = new \ReflectionClass( get_class($exception) );
514
515
        // file
516
        if (isset($trace_location['file'])) {
517
            $reflectionProperty = $reflectionClass->getProperty('file');
518
            $reflectionProperty->setAccessible(true);
519
            $reflectionProperty->setValue($exception, $trace_location['file']);
520
        }
521
522
        // line
523
        if (isset($trace_location['line'])) {
524
            $reflectionProperty = $reflectionClass->getProperty('line');
525
            $reflectionProperty->setAccessible(true);
526
            $reflectionProperty->setValue($exception, $trace_location['line']);
527
        }
528
529
        throw $exception;
530
    }
531
532
    /**
533
     * @param  mixed $value
534
     * @return bool  Is the $value traversable or not
535
     * @throws InvalidArgumentException
536
     *
537
     * @todo   NotTraversableException
538
     */
539
    public static function mustBeTraversable($value)
540
    {
541
        if (static::isTraversable($value))
542
            return true;
543
544
        $exception = new \InvalidArgumentException(
545
            "A value must be Traversable instead of: \n"
546
            .var_export($value, true)
547
        );
548
549
        // The true location of the throw is still available through the backtrace
550
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
551
        $reflectionClass = new \ReflectionClass( get_class($exception) );
552
553
        // file
554
        if (isset($trace_location['file'])) {
555
            $reflectionProperty = $reflectionClass->getProperty('file');
556
            $reflectionProperty->setAccessible(true);
557
            $reflectionProperty->setValue($exception, $trace_location['file']);
558
        }
559
560
        // line
561
        if (isset($trace_location['line'])) {
562
            $reflectionProperty = $reflectionClass->getProperty('line');
563
            $reflectionProperty->setAccessible(true);
564
            $reflectionProperty->setValue($exception, $trace_location['line']);
565
        }
566
567
        throw $exception;
568
    }
569
570
    /**
571
     * Generates an id usable in hashes to identify a single grouped row.
572
     *
573
     * @param array $row    The row of the array to group by.
574
     * @param array $groups A list of the different groups. Groups can be
575
     *                      strings describing a column name or a callable
576
     *                      function, an array representing a callable,
577
     *                      a function or an integer representing a column.
578
     *                      If the index of the group is a string, it will
579
     *                      be used as a prefix for the group name.
580
     *                      Example:
581
     *                      [
582
     *                          'column_name',
583
     *                          'function_to_call',
584
     *                          4,  //column_number
585
     *                          'group_prefix'  => function($row){},
586
     *                          'group_prefix2' => [$object, 'method'],
587
     *                      ]
588
     *
589
     * @return string       The unique identifier of the group
590
     */
591
    public static function generateGroupId($row, array $groups_definitions, array $options=[])
592
    {
593
        Arrays::mustBeCountable($row);
594
595
        $key_value_separator = ! empty($options['key_value_separator'])
596
                             ? $options['key_value_separator']
597
                             : ':'
598
                             ;
599
600
        $groups_separator    = ! empty($options['groups_separator'])
601
                             ? $options['groups_separator']
602
                             : '-'
603
                             ;
604
605
        $group_parts = [];
606
        foreach ($groups_definitions as $group_definition_key => $group_definition_value) {
607
            $part_name = '';
608
609
            if (is_string($group_definition_key)) {
610
                $part_name .= $group_definition_key.'_';
611
            }
612
613
            if (is_string($group_definition_value)) {
614
                if (    (is_array($row)              && ! array_key_exists($group_definition_value, $row))
615
                    ||  ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value))
616
                ) {
617
                    throw new UsageException(
618
                        'Unset column for group id generation: '
619
                        .var_export($group_definition_value, true)
620
                        ."\n" . var_export($row, true)
621
                    );
622
                }
623
624
                $part_name         .= $group_definition_value;
625
                $group_result_value = $row[ $group_definition_value ];
626
            }
627
            elseif (is_int($group_definition_value)) {
628
                if (    (is_array($row)              && ! array_key_exists($group_definition_value, $row))
629
                    ||  ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value))
630
                ) {
631
                    throw new UsageException(
632
                        'Unset column for group id generation: '
633
                        .var_export($group_definition_value, true)
634
                        ."\n" . var_export($row, true)
635
                    );
636
                }
637
638
                $part_name         .= $group_definition_value ? : '0';
639
                $group_result_value = $row[ $group_definition_value ];
640
            }
641
            elseif (is_callable($group_definition_value)) {
642
643
                if (is_string($group_definition_value)) {
644
                    $part_name .= $group_definition_value;
645
                }
646
                // elseif (is_function($value)) {
647
                elseif (is_object($group_definition_value) && ($group_definition_value instanceof \Closure)) {
648
                    $part_name .= 'unnamed-closure-'
649
                                . hash('crc32b', var_export($group_definition_value, true));
650
                }
651
                elseif (is_array($group_definition_value)) {
652
                    $part_name .= implode('::', $group_definition_value);
653
                }
654
655
                $group_result_value = call_user_func_array($group_definition_value, [
656
                    $row, &$part_name
657
                ]);
658
            }
659
            else {
660
                throw new UsageException(
661
                    'Bad value provided for group id generation: '
662
                    .var_export($group_definition_value, true)
663
                    ."\n" . var_export($row, true)
664
                );
665
            }
666
667
            if (!is_null($part_name))
668
                $group_parts[ $part_name ] = $group_result_value;
669
        }
670
671
        // sort the groups by names (without it the same group could have multiple ids)
672
        ksort($group_parts);
673
674
        // bidimensional implode
675
        $out = [];
676
        foreach ($group_parts as $group_name => $group_value) {
677
            if (is_object($group_value)) {
678
                $group_value = get_class($group_value)
679
                             . '_'
680
                             . hash( 'crc32b', var_export($group_value, true) );
681
            }
682
            elseif (is_array($group_value)) {
683
                $group_value = 'array_' . hash( 'crc32b', var_export($group_value, true) );
684
            }
685
686
            $out[] = $group_name . $key_value_separator . $group_value;
687
        }
688
689
        return implode($groups_separator, $out);
690
    }
691
692
    /**/
693
}
694