Passed
Push — master ( 6a4583...107afa )
by Jean
02:44
created

Arrays::isAssociative()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 8
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
     * @param  mixed $value
456
     * @return bool  Is the $value countable or not
457
     * @throws InvalidArgumentException
458
     *
459
     * @todo   NotCountableException
460
     */
461
    public static function mustBeCountable($value)
462
    {
463
        if (static::isCountable($value))
464
            return true;
465
466
        $exception = new \InvalidArgumentException(
467
            "A value must be Countable instead of: \n"
468
            .var_export($value, true)
469
        );
470
471
        // The true location of the throw is still available through the backtrace
472
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
473
        $reflectionClass = new \ReflectionClass( get_class($exception) );
474
475
        // file
476
        if (isset($trace_location['file'])) {
477
            $reflectionProperty = $reflectionClass->getProperty('file');
478
            $reflectionProperty->setAccessible(true);
479
            $reflectionProperty->setValue($exception, $trace_location['file']);
480
        }
481
482
        // line
483
        if (isset($trace_location['line'])) {
484
            $reflectionProperty = $reflectionClass->getProperty('line');
485
            $reflectionProperty->setAccessible(true);
486
            $reflectionProperty->setValue($exception, $trace_location['line']);
487
        }
488
489
        throw $exception;
490
    }
491
492
    /**
493
     * @param  mixed $value
494
     * @return bool  Is the $value traversable or not
495
     * @throws InvalidArgumentException
496
     *
497
     * @todo   NotTraversableException
498
     */
499
    public static function mustBeTraversable($value)
500
    {
501
        if (static::isTraversable($value))
502
            return true;
503
504
        $exception = new \InvalidArgumentException(
505
            "A value must be Traversable instead of: \n"
506
            .var_export($value, true)
507
        );
508
509
        // The true location of the throw is still available through the backtrace
510
        $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
511
        $reflectionClass = new \ReflectionClass( get_class($exception) );
512
513
        // file
514
        if (isset($trace_location['file'])) {
515
            $reflectionProperty = $reflectionClass->getProperty('file');
516
            $reflectionProperty->setAccessible(true);
517
            $reflectionProperty->setValue($exception, $trace_location['file']);
518
        }
519
520
        // line
521
        if (isset($trace_location['line'])) {
522
            $reflectionProperty = $reflectionClass->getProperty('line');
523
            $reflectionProperty->setAccessible(true);
524
            $reflectionProperty->setValue($exception, $trace_location['line']);
525
        }
526
527
        throw $exception;
528
    }
529
530
    /**
531
     * Generates an id usable in hashes to identify a single grouped row.
532
     *
533
     * @param array $row    The row of the array to group by.
534
     * @param array $groups A list of the different groups. Groups can be
535
     *                      strings describing a column name or a callable
536
     *                      function, an array representing a callable,
537
     *                      a function or an integer representing a column.
538
     *                      If the index of the group is a string, it will
539
     *                      be used as a prefix for the group name.
540
     *                      Example:
541
     *                      [
542
     *                          'column_name',
543
     *                          'function_to_call',
544
     *                          4,  //column_number
545
     *                          'group_prefix'  => function($row){},
546
     *                          'group_prefix2' => [$object, 'method'],
547
     *                      ]
548
     *
549
     * @return string       The unique identifier of the group
550
     */
551
    public static function generateGroupId($row, array $groups_definitions, array $options=[])
552
    {
553
        Arrays::mustBeCountable($row);
554
555
        $key_value_separator = ! empty($options['key_value_separator'])
556
                             ? $options['key_value_separator']
557
                             : ':'
558
                             ;
559
560
        $groups_separator    = ! empty($options['groups_separator'])
561
                             ? $options['groups_separator']
562
                             : '-'
563
                             ;
564
565
        $group_parts = [];
566
        foreach ($groups_definitions as $group_definition_key => $group_definition_value) {
567
            $part_name = '';
568
569
            if (is_string($group_definition_key)) {
570
                $part_name .= $group_definition_key.'_';
571
            }
572
573
            if (is_string($group_definition_value) && array_key_exists($group_definition_value, $row)) {
574
                $part_name         .= $group_definition_value;
575
                $group_result_value = $row[ $group_definition_value ];
576
            }
577
            elseif (is_int($group_definition_value)) {
578
                $part_name         .= $group_definition_value ? : '0';
579
                $group_result_value = $row[ $group_definition_value ];
580
            }
581
            /* TODO check this is not just dead code * /
582
            elseif (is_callable($group_definition_value)) {
583
584
                if (is_string($group_definition_value)) {
585
                    $part_name .= $group_definition_value;
586
                }
587
                // elseif (is_function($value)) {
588
                elseif (is_object($value) && ($value instanceof \Closure)) {
589
                    $part_name .= 'unnamed-closure-'
590
                                . hash('crc32b', var_export($group_definition_value, true));
591
                }
592
                elseif (is_array($group_definition_value)) {
593
                    $part_name .= implode('::', $group_definition_value);
594
                }
595
596
                $group_result_value = call_user_func_array($group_definition_value, [
597
                    $row, &$part_name
598
                ]);
599
            }
600
            /**/
601
            else {
602
                self::throwUsageException(
0 ignored issues
show
Bug introduced by
The method throwUsageException() does not exist on JClaveau\Arrays\Arrays. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

602
                self::/** @scrutinizer ignore-call */ 
603
                      throwUsageException(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
603
                    'Bad value provided for groupBy id generation: '
604
                    .var_export($group_definition_value, true)
605
                    ."\n" . var_export($row, true)
606
                );
607
            }
608
609
            if (!is_null($part_name))
610
                $group_parts[ $part_name ] = $group_result_value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $group_result_value does not seem to be defined for all execution paths leading up to this point.
Loading history...
611
        }
612
613
        // sort the groups by names (without it the same group could have multiple ids)
614
        ksort($group_parts);
615
616
        // bidimensional implode
617
        $out = [];
618
        foreach ($group_parts as $group_name => $group_value) {
619
            if (is_object($group_value)) {
620
                $group_value = get_class($group_value)
621
                             . '_'
622
                             . hash( 'crc32b', var_export($group_value, true) );
623
            }
624
            elseif (is_array($group_value)) {
625
                $group_value = 'array_' . hash( 'crc32b', var_export($group_value, true) );
626
            }
627
628
            $out[] = $group_name . $key_value_separator . $group_value;
629
        }
630
631
        return implode($groups_separator, $out);
632
    }
633
634
    /**/
635
}
636