Completed
Push — master ( 7732b1...2a9c77 )
by Jean
02:09
created

Arrays::cleanMergeDuplicates()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
nc 11
nop 2
dl 0
loc 27
rs 8.4444
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
        array $existing_row,
198
        array $conflict_row
199
    ){
200
        $merge = static::mergeRecursiveCustom(
201
            $existing_row,
202
            $conflict_row,
203
            function ($existing_value, $conflict_value, $column) {
204
205
                if ( ! $existing_value instanceof MergeBucket) {
206
                    $existing_value = MergeBucket::from()->push($existing_value);
207
                }
208
209
                // We store the new value with their previous ones
210
                if ( ! $conflict_value instanceof MergeBucket) {
211
                    $conflict_value = MergeBucket::from()->push($conflict_value);
212
                }
213
214
                foreach ($conflict_value->toArray() as $conflict_key => $conflict_entry) {
215
                    $existing_value->push($conflict_entry);
216
                }
217
218
                return $existing_value;
219
            },
220
            0
221
        );
222
223
        return $merge;
224
    }
225
226
    /**
227
     * This is the cleaning part of self::mergePreservingDistincts()
228
     *
229
     * @param  array|Traversable $row
0 ignored issues
show
Bug introduced by
The type JClaveau\Arrays\Traversable was not found. Did you mean Traversable? If so, make sure to prefix the type with \.
Loading history...
230
     * @param  array             $options : 'excluded_columns'
231
     */
232
    public static function cleanMergeDuplicates($row, array $options=[])
233
    {
234
        if ( ! is_array($row) && ! $row instanceof \Traversable) {
235
            throw new \InvalidArgumentException(
236
                "\$row must be an array or a \Traversable instead of: \n"
237
                .var_export($row, true)
238
            );
239
        }
240
241
        $excluded_columns = isset($options['excluded_columns'])
242
                          ? $options['excluded_columns']
243
                          : []
244
                          ;
245
246
        foreach ($row as $column => &$values) {
247
            if ( ! $values instanceof MergeBucket)
248
                continue;
249
250
            if (in_array($column, $excluded_columns))
251
                continue;
252
253
            $values = Arrays::unique($values);
254
            if (count($values) == 1)
255
                $values = $values[0];
256
        }
257
258
        return $row;
259
    }
260
261
    /**
262
     * This is the cleaning part of self::mergePreservingDistincts()
263
     *
264
     * @param  array|Traversable $row
265
     * @param  array             $options : 'excluded_columns'
266
     *
267
     * @see mergePreservingDistincts()
268
     */
269
    public static function cleanMergeBuckets($row, array $options=[])
270
    {
271
        if ( ! is_array($row) && ! $row instanceof \Traversable) {
272
            throw new \InvalidArgumentException(
273
                "\$row must be an array or a \Traversable instead of: \n"
274
                .var_export($row, true)
275
            );
276
        }
277
278
        $excluded_columns = isset($options['excluded_columns'])
279
                          ? $options['excluded_columns']
280
                          : []
281
                          ;
282
283
        foreach ($row as $column => &$values) {
284
            if (in_array($column, $excluded_columns))
285
                continue;
286
287
            if ($values instanceof MergeBucket)
288
                $values = $values->toArray();
289
        }
290
291
        return $row;
292
    }
293
294
    /**
295
     * Replacement of array_unique, keeping the first key.
296
     *
297
     * @param  array|\Traversable $array
298
     * @return array|\Traversable With unique values
299
     *
300
     * @todo   Options to keep another key than the first one?
301
     */
302
    public static function unique($array)
303
    {
304
        if (! is_array($array) && ! $array instanceof \Traversable) {
0 ignored issues
show
introduced by
$array is always a sub-type of Traversable.
Loading history...
305
            throw new \InvalidArgumentException(
306
                "\$array must be an array or a \Traversable instead of: \n"
307
                .var_export($array, true)
308
            );
309
        }
310
311
        $ids = [];
312
        foreach ($array as $key => $value) {
313
            if (is_scalar($value)) {
314
                $id = $value;
315
            }
316
            else {
317
                $id = serialize($value);
318
            }
319
320
            if (isset($ids[ $id ])) {
321
                unset($array[ $key ]);
322
                $ids[ $id ][] = $key;
323
                continue;
324
            }
325
326
            $ids[ $id ] = [$key];
327
        }
328
329
        return $array;
330
    }
331
332
    /**
333
     * Replacement of array_sum wich throws exceptions instead of skipping
334
     * bad operands.
335
     *
336
     * @param  array|\Traversable $array
337
     * @return int|double         The sum
338
     *
339
     * @todo   Support options like 'strict', 'skip_non_scalars', 'native'
340
     */
341
    public static function sum($array)
342
    {
343
        if (! is_array($array) && ! $array instanceof \Traversable) {
0 ignored issues
show
introduced by
$array is always a sub-type of Traversable.
Loading history...
344
            throw new \InvalidArgumentException(
345
                "\$array must be an array or a \Traversable instead of: \n"
346
                .var_export($array, true)
347
            );
348
        }
349
350
        $sum = 0;
351
        foreach ($array as $key => &$value) { // &for optimization
352
            if (is_scalar($value)) {
353
                $sum += $value;
354
            }
355
            elseif (is_null($value)) {
356
                continue;
357
            }
358
            elseif (is_array($value)) {
359
                throw new \InvalidArgumentException(
360
                    "Trying to sum an array with '$sum': ".var_export($value, true)
361
                );
362
            }
363
            elseif (is_object($value)) {
364
                if ( ! method_exists($value, 'toNumber')) {
365
                    throw new \InvalidArgumentEXception(
366
                         "Trying to sum a ".get_class($value)." object which cannot be casted as a number. "
367
                        ."Please add a toNumber() method."
368
                    );
369
                }
370
371
                $sum += $value->toNumber();
372
            }
373
        }
374
375
        return $sum;
376
    }
377
378
    /**
379
     * This method returns a classical mathemartic weighted mean.
380
     *
381
     * @todo It would ideally handled by a bridge with this fantastic math
382
     * lib https://github.com/markrogoyski/math-php/ but we need the support
383
     * of PHP 7 first.
384
     *
385
     * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
386
     * @see https://github.com/markrogoyski/math-php/
387
     */
388
    public static function weightedMean($values, $weights)
389
    {
390
        if ($values instanceof ChainableArray)
391
            $values = $values->toArray();
392
393
        if ($weights instanceof ChainableArray)
394
            $weights = $weights->toArray();
395
396
        if ( ! is_array($values))
397
            $values = [$values];
398
399
        if ( ! is_array($weights))
400
            $weights = [$weights];
401
402
        if (count($values) != count($weights)) {
403
            throw new \InvalidArgumentException(
404
                "Different number of "
405
                ." values and weights for weight mean calculation: \n"
406
                .var_export($values,  true)."\n\n"
407
                .var_export($weights, true)
408
            );
409
        }
410
411
        if (!$values)
412
            return null;
413
414
        $weights_sum  = array_sum($weights);
415
        if (!$weights_sum)
416
            return 0;
417
418
        $weighted_sum = 0;
419
        foreach ($values as $i => $value) {
420
            $weighted_sum += $value * $weights[$i];
421
        }
422
423
        return $weighted_sum / $weights_sum;
424
    }
425
426
    /**/
427
}
428