Passed
Push — master ( 2a9c77...ac68e5 )
by Jean
02:19
created

Arrays::isCountable()   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
        array $existing_row,
136
        array $conflict_row,
137
        callable $merge_resolver=null,
138
        $max_depth=null
139
    ){
140
        foreach ($conflict_row as $column => $conflict_value) {
141
142
            // not existing in first array
143
            if (!isset($existing_row[$column])) {
144
                $existing_row[$column] = $conflict_value;
145
                continue;
146
            }
147
148
            $existing_value = $existing_row[$column];
149
150
            // two arrays so we recurse
151
            if (is_array($existing_value) && is_array($conflict_value)) {
152
153
                if ($max_depth === null || $max_depth > 0) {
154
                    $existing_row[$column] = static::mergeRecursiveCustom(
155
                        $existing_value,
156
                        $conflict_value,
157
                        $merge_resolver,
158
                        $max_depth - 1
159
                    );
160
                    continue;
161
                }
162
            }
163
164
            if ($merge_resolver) {
165
                $existing_row[$column] = call_user_func_array(
166
                    $merge_resolver,
167
                    [
168
                        $existing_value,
169
                        $conflict_value,
170
                        $column,
171
                    ]
172
                );
173
            }
174
            else {
175
                // same resolution as array_merge_recursive
176
                if (!is_array($existing_value)) {
177
                    $existing_row[$column] = [$existing_value];
178
                }
179
180
                // We store the new value with their previous ones
181
                $existing_row[$column][] = $conflict_value;
182
            }
183
        }
184
185
        return $existing_row;
186
    }
187
188
    /**
189
     * Merges two rows
190
     *
191
     * @param  array $existing_row
192
     * @param  array $conflict_row
193
     *
194
     * @return array
195
     */
196
    public static function mergePreservingDistincts(
197
        $existing_row,
198
        $conflict_row
199
    ){
200
        static::mustBeCountable($existing_row);
201
        static::mustBeCountable($conflict_row);
202
203
        $merge = static::mergeRecursiveCustom(
204
            $existing_row,
205
            $conflict_row,
206
            function ($existing_value, $conflict_value, $column) {
207
208
                if ( ! $existing_value instanceof MergeBucket) {
209
                    $existing_value = MergeBucket::from()->push($existing_value);
210
                }
211
212
                // We store the new value with their previous ones
213
                if ( ! $conflict_value instanceof MergeBucket) {
214
                    $conflict_value = MergeBucket::from()->push($conflict_value);
215
                }
216
217
                foreach ($conflict_value->toArray() as $conflict_key => $conflict_entry) {
218
                    $existing_value->push($conflict_entry);
219
                }
220
221
                return $existing_value;
222
            },
223
            0
224
        );
225
226
        return $merge;
227
    }
228
229
    /**
230
     * This is the cleaning part of self::mergePreservingDistincts()
231
     *
232
     * @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...
233
     * @param  array             $options : 'excluded_columns'
234
     */
235
    public static function cleanMergeDuplicates($row, array $options=[])
236
    {
237
        static::mustBeCountable($row);
238
239
        $excluded_columns = isset($options['excluded_columns'])
240
                          ? $options['excluded_columns']
241
                          : []
242
                          ;
243
244
        foreach ($row as $column => &$values) {
245
            if ( ! $values instanceof MergeBucket)
246
                continue;
247
248
            if (in_array($column, $excluded_columns))
249
                continue;
250
251
            $values = Arrays::unique($values);
252
            if (count($values) == 1)
253
                $values = $values[0];
254
        }
255
256
        return $row;
257
    }
258
259
    /**
260
     * This is the cleaning last part of self::mergePreservingDistincts()
261
     *
262
     * @param  array|Countable   $row
263
     * @param  array             $options : 'excluded_columns'
264
     *
265
     * @see mergePreservingDistincts()
266
     * @see cleanMergeDuplicates()
267
     */
268
    public static function cleanMergeBuckets($row, array $options=[])
269
    {
270
        static::mustBeCountable($row);
271
272
        $excluded_columns = isset($options['excluded_columns'])
273
                          ? $options['excluded_columns']
274
                          : []
275
                          ;
276
277
        foreach ($row as $column => &$values) {
278
            if (in_array($column, $excluded_columns))
279
                continue;
280
281
            if ($values instanceof MergeBucket)
282
                $values = $values->toArray();
283
        }
284
285
        return $row;
286
    }
287
288
    /**
289
     * Replacement of array_unique, keeping the first key.
290
     *
291
     * @param  array|\Traversable $array
292
     * @return array|\Traversable With unique values
293
     *
294
     * @todo   Options to keep another key than the first one?
295
     */
296
    public static function unique($array)
297
    {
298
        static::mustBeCountable($array);
299
300
        $ids = [];
301
        foreach ($array as $key => $value) {
302
            if (is_scalar($value)) {
303
                $id = $value;
304
            }
305
            else {
306
                $id = serialize($value);
307
            }
308
309
            if (isset($ids[ $id ])) {
310
                unset($array[ $key ]);
311
                $ids[ $id ][] = $key;
312
                continue;
313
            }
314
315
            $ids[ $id ] = [$key];
316
        }
317
318
        return $array;
319
    }
320
321
    /**
322
     * Replacement of array_sum wich throws exceptions instead of skipping
323
     * bad operands.
324
     *
325
     * @param  array|\Traversable $array
326
     * @return int|double         The sum
327
     *
328
     * @todo   Support options like 'strict', 'skip_non_scalars', 'native'
329
     */
330
    public static function sum($array)
331
    {
332
        static::mustBeCountable($array);
333
334
        $sum = 0;
335
        foreach ($array as $key => &$value) { // &for optimization
336
            if (is_scalar($value)) {
337
                $sum += $value;
338
            }
339
            elseif (is_null($value)) {
340
                continue;
341
            }
342
            elseif (is_array($value)) {
343
                throw new \InvalidArgumentException(
344
                    "Trying to sum an array with '$sum': ".var_export($value, true)
345
                );
346
            }
347
            elseif (is_object($value)) {
348
                if ( ! method_exists($value, 'toNumber')) {
349
                    throw new \InvalidArgumentEXception(
350
                         "Trying to sum a ".get_class($value)." object which cannot be casted as a number. "
351
                        ."Please add a toNumber() method."
352
                    );
353
                }
354
355
                $sum += $value->toNumber();
356
            }
357
        }
358
359
        return $sum;
360
    }
361
362
    /**
363
     * This method returns a classical mathemartic weighted mean.
364
     *
365
     * @todo It would ideally handled by a bridge with this fantastic math
366
     * lib https://github.com/markrogoyski/math-php/ but we need the support
367
     * of PHP 7 first.
368
     *
369
     * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
370
     * @see https://github.com/markrogoyski/math-php/
371
     */
372
    public static function weightedMean($values, $weights)
373
    {
374
        if ($values instanceof ChainableArray)
375
            $values = $values->toArray();
376
377
        if ($weights instanceof ChainableArray)
378
            $weights = $weights->toArray();
379
380
        if ( ! is_array($values))
381
            $values = [$values];
382
383
        if ( ! is_array($weights))
384
            $weights = [$weights];
385
386
        if (count($values) != count($weights)) {
387
            throw new \InvalidArgumentException(
388
                "Different number of "
389
                ." values and weights for weight mean calculation: \n"
390
                .var_export($values,  true)."\n\n"
391
                .var_export($weights, true)
392
            );
393
        }
394
395
        if (!$values)
396
            return null;
397
398
        $weights_sum  = array_sum($weights);
399
        if (!$weights_sum)
400
            return 0;
401
402
        $weighted_sum = 0;
403
        foreach ($values as $i => $value) {
404
            $weighted_sum += $value * $weights[$i];
405
        }
406
407
        return $weighted_sum / $weights_sum;
408
    }
409
410
    /**
411
     * This is not required anymore with PHP 7.
412
     *
413
     * @return bool
414
     */
415
    public static function isTraversable($value)
416
    {
417
        return $value instanceof \Traversable || is_array($value);
418
    }
419
420
    /**
421
     * This is not required anymore with PHP 7.
422
     *
423
     * @return bool
424
     */
425
    public static function isCountable($value)
426
    {
427
        return $value instanceof \Countable || is_array($value);
428
    }
429
430
    /**
431
     */
432
    public static function mustBeCountable($value)
433
    {
434
        if ( ! static::isCountable($value) ) {
435
            $exception = new \InvalidArgumentException(
436
                "A value must be Countable instead of: \n"
437
                .var_export($value, true)
438
            );
439
440
            // The true location of the throw is still available through the backtrace
441
            $trace_location  = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
442
            $reflectionClass = new \ReflectionClass( get_class($exception) );
443
444
            //file
445
            if (isset($trace_location['file'])) {
446
                $reflectionProperty = $reflectionClass->getProperty('file');
447
                $reflectionProperty->setAccessible(true);
448
                $reflectionProperty->setValue($exception, $trace_location['file']);
449
            }
450
451
            // line
452
            if (isset($trace_location['line'])) {
453
                $reflectionProperty = $reflectionClass->getProperty('line');
454
                $reflectionProperty->setAccessible(true);
455
                $reflectionProperty->setValue($exception, $trace_location['line']);
456
            }
457
458
            throw $exception;
459
        }
460
    }
461
462
    /**/
463
}
464