1 | <?php |
||
2 | namespace JClaveau\Arrays; |
||
3 | |||
4 | use JClaveau\Exceptions\UsageException; |
||
5 | |||
6 | /** |
||
7 | * |
||
8 | */ |
||
9 | class Arrays |
||
10 | { |
||
11 | use Arrays_Merge_Trait; |
||
12 | |||
13 | /** |
||
14 | * Taken from Kohana's Arr class. |
||
15 | * |
||
16 | * Tests if an array is associative or not. |
||
17 | * |
||
18 | * // Returns TRUE |
||
19 | * Arr::isAssoc(array('username' => 'john.doe')); |
||
20 | * |
||
21 | * // Returns FALSE |
||
22 | * Arr::isAssoc('foo', 'bar'); |
||
23 | * |
||
24 | * @param array $array array to check |
||
25 | * @return boolean |
||
26 | */ |
||
27 | public static function isAssociative(array $array) |
||
28 | { |
||
29 | // Keys of the array |
||
30 | $keys = array_keys($array); |
||
31 | |||
32 | // If the array keys of the keys match the keys, then the array must |
||
33 | // not be associative (e.g. the keys array looked like {0:0, 1:1...}). |
||
34 | return array_keys($keys) !== $keys; |
||
35 | } |
||
36 | |||
37 | /** |
||
38 | * Replacement of array_unique, keeping the first key. |
||
39 | * |
||
40 | * @param array|\Traversable $array |
||
41 | * @return array|\Traversable With unique values |
||
42 | * |
||
43 | * @todo Options to keep another key than the first one? |
||
44 | */ |
||
45 | public static function unique($array) |
||
46 | { |
||
47 | static::mustBeCountable($array); |
||
48 | |||
49 | $ids = []; |
||
50 | foreach ($array as $key => $value) { |
||
51 | if (is_scalar($value)) { |
||
52 | $id = $value; |
||
53 | } |
||
54 | else { |
||
55 | $id = serialize($value); |
||
56 | } |
||
57 | |||
58 | if (isset($ids[ $id ])) { |
||
59 | unset($array[ $key ]); |
||
60 | $ids[ $id ][] = $key; |
||
61 | continue; |
||
62 | } |
||
63 | |||
64 | $ids[ $id ] = [$key]; |
||
65 | } |
||
66 | |||
67 | return $array; |
||
68 | } |
||
69 | |||
70 | /** |
||
71 | */ |
||
72 | public static function keyExists($key, $array) |
||
73 | { |
||
74 | static::mustBeTraversable($array); |
||
75 | |||
76 | if (is_array($array)) { |
||
77 | return array_key_exists($key, $array); |
||
78 | } |
||
79 | elseif ($array instanceof ChainableArray || method_exists($array, 'keyExists')) { |
||
80 | return $array->keyExists($key); |
||
81 | } |
||
82 | else { |
||
83 | throw new \InvalidArgumentException( |
||
84 | "keyExists() method missing on :\n". var_export($array, true) |
||
85 | ); |
||
86 | } |
||
87 | |||
88 | return $array; |
||
0 ignored issues
–
show
|
|||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Replacement of array_sum wich throws exceptions instead of skipping |
||
93 | * bad operands. |
||
94 | * |
||
95 | * @param array|\Traversable $array |
||
96 | * @return int|double The sum |
||
97 | * |
||
98 | * @todo Support options like 'strict', 'skip_non_scalars', 'native' |
||
99 | */ |
||
100 | public static function sum($array) |
||
101 | { |
||
102 | static::mustBeCountable($array); |
||
103 | |||
104 | $sum = 0; |
||
105 | foreach ($array as $key => &$value) { // &for optimization |
||
106 | if (is_scalar($value)) { |
||
107 | $sum += $value; |
||
108 | } |
||
109 | elseif (is_null($value)) { |
||
110 | continue; |
||
111 | } |
||
112 | elseif (is_array($value)) { |
||
113 | throw new \InvalidArgumentException( |
||
114 | "Trying to sum an array with '$sum': ".var_export($value, true) |
||
115 | ); |
||
116 | } |
||
117 | elseif (is_object($value)) { |
||
118 | if ( ! method_exists($value, 'toNumber')) { |
||
119 | throw new \InvalidArgumentEXception( |
||
120 | "Trying to sum a ".get_class($value)." object which cannot be casted as a number. " |
||
121 | ."Please add a toNumber() method." |
||
122 | ); |
||
123 | } |
||
124 | |||
125 | $sum += $value->toNumber(); |
||
126 | } |
||
127 | } |
||
128 | |||
129 | return $sum; |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * This method returns a classical mathemartic weighted mean. |
||
134 | * |
||
135 | * @todo It would ideally handled by a bridge with this fantastic math |
||
136 | * lib https://github.com/markrogoyski/math-php/ but we need the support |
||
137 | * of PHP 7 first. |
||
138 | * |
||
139 | * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean |
||
140 | * @see https://github.com/markrogoyski/math-php/ |
||
141 | */ |
||
142 | public static function weightedMean($values, $weights) |
||
143 | { |
||
144 | if ($values instanceof ChainableArray) |
||
145 | $values = $values->toArray(); |
||
146 | |||
147 | if ($weights instanceof ChainableArray) |
||
148 | $weights = $weights->toArray(); |
||
149 | |||
150 | if ( ! is_array($values)) |
||
151 | $values = [$values]; |
||
152 | |||
153 | if ( ! is_array($weights)) |
||
154 | $weights = [$weights]; |
||
155 | |||
156 | if (count($values) != count($weights)) { |
||
157 | throw new \InvalidArgumentException( |
||
158 | "Different number of " |
||
159 | ." values and weights for weight mean calculation: \n" |
||
160 | .var_export($values, true)."\n\n" |
||
161 | .var_export($weights, true) |
||
162 | ); |
||
163 | } |
||
164 | |||
165 | if (!$values) |
||
166 | return null; |
||
167 | |||
168 | $weights_sum = array_sum($weights); |
||
169 | if (!$weights_sum) |
||
170 | return 0; |
||
171 | |||
172 | $weighted_sum = 0; |
||
173 | foreach ($values as $i => $value) { |
||
174 | $weighted_sum += $value * $weights[$i]; |
||
175 | } |
||
176 | |||
177 | return $weighted_sum / $weights_sum; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * This is not required anymore with PHP 7. |
||
182 | * |
||
183 | * @return bool |
||
184 | */ |
||
185 | public static function isTraversable($value) |
||
186 | { |
||
187 | return $value instanceof \Traversable || is_array($value); |
||
188 | } |
||
189 | |||
190 | /** |
||
191 | * This is not required anymore with PHP 7. |
||
192 | * |
||
193 | * @return bool |
||
194 | */ |
||
195 | public static function isCountable($value) |
||
196 | { |
||
197 | return $value instanceof \Countable || is_array($value); |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * @param mixed $value |
||
202 | * @return bool Is the $value countable or not |
||
203 | * @throws InvalidArgumentException |
||
204 | * |
||
205 | * @todo NotCountableException |
||
206 | */ |
||
207 | public static function mustBeCountable($value) |
||
208 | { |
||
209 | if (static::isCountable($value)) |
||
210 | return true; |
||
211 | |||
212 | $exception = new \InvalidArgumentException( |
||
213 | "A value must be Countable instead of: \n" |
||
214 | .var_export($value, true) |
||
215 | ); |
||
216 | |||
217 | // The true location of the throw is still available through the backtrace |
||
218 | $trace_location = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; |
||
219 | $reflectionClass = new \ReflectionClass( get_class($exception) ); |
||
220 | |||
221 | // file |
||
222 | if (isset($trace_location['file'])) { |
||
223 | $reflectionProperty = $reflectionClass->getProperty('file'); |
||
224 | $reflectionProperty->setAccessible(true); |
||
225 | $reflectionProperty->setValue($exception, $trace_location['file']); |
||
226 | } |
||
227 | |||
228 | // line |
||
229 | if (isset($trace_location['line'])) { |
||
230 | $reflectionProperty = $reflectionClass->getProperty('line'); |
||
231 | $reflectionProperty->setAccessible(true); |
||
232 | $reflectionProperty->setValue($exception, $trace_location['line']); |
||
233 | } |
||
234 | |||
235 | throw $exception; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * @param mixed $value |
||
240 | * @return bool Is the $value traversable or not |
||
241 | * @throws InvalidArgumentException |
||
242 | * |
||
243 | * @todo NotTraversableException |
||
244 | */ |
||
245 | public static function mustBeTraversable($value) |
||
246 | { |
||
247 | if (static::isTraversable($value)) |
||
248 | return true; |
||
249 | |||
250 | $exception = new \InvalidArgumentException( |
||
251 | "A value must be Traversable instead of: \n" |
||
252 | .var_export($value, true) |
||
253 | ); |
||
254 | |||
255 | // The true location of the throw is still available through the backtrace |
||
256 | $trace_location = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]; |
||
257 | $reflectionClass = new \ReflectionClass( get_class($exception) ); |
||
258 | |||
259 | // file |
||
260 | if (isset($trace_location['file'])) { |
||
261 | $reflectionProperty = $reflectionClass->getProperty('file'); |
||
262 | $reflectionProperty->setAccessible(true); |
||
263 | $reflectionProperty->setValue($exception, $trace_location['file']); |
||
264 | } |
||
265 | |||
266 | // line |
||
267 | if (isset($trace_location['line'])) { |
||
268 | $reflectionProperty = $reflectionClass->getProperty('line'); |
||
269 | $reflectionProperty->setAccessible(true); |
||
270 | $reflectionProperty->setValue($exception, $trace_location['line']); |
||
271 | } |
||
272 | |||
273 | throw $exception; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Generates an id usable in hashes to identify a single grouped row. |
||
278 | * |
||
279 | * @param array $row The row of the array to group by. |
||
280 | * @param array $groups A list of the different groups. Groups can be |
||
281 | * strings describing a column name or a callable |
||
282 | * function, an array representing a callable, |
||
283 | * a function or an integer representing a column. |
||
284 | * If the index of the group is a string, it will |
||
285 | * be used as a prefix for the group name. |
||
286 | * Example: |
||
287 | * [ |
||
288 | * 'column_name', |
||
289 | * 'function_to_call', |
||
290 | * 4, //column_number |
||
291 | * 'group_prefix' => function($row){}, |
||
292 | * 'group_prefix2' => [$object, 'method'], |
||
293 | * ] |
||
294 | * |
||
295 | * @return string The unique identifier of the group |
||
296 | */ |
||
297 | public static function generateGroupId($row, array $groups_definitions, array $options=[]) |
||
298 | { |
||
299 | Arrays::mustBeCountable($row); |
||
300 | |||
301 | $key_value_separator = ! empty($options['key_value_separator']) |
||
302 | ? $options['key_value_separator'] |
||
303 | : ':' |
||
304 | ; |
||
305 | |||
306 | $groups_separator = ! empty($options['groups_separator']) |
||
307 | ? $options['groups_separator'] |
||
308 | : '-' |
||
309 | ; |
||
310 | |||
311 | $group_parts = []; |
||
312 | foreach ($groups_definitions as $group_definition_key => $group_definition_value) { |
||
313 | $part_name = ''; |
||
314 | |||
315 | if (is_string($group_definition_key)) { |
||
316 | $part_name .= $group_definition_key.'_'; |
||
317 | } |
||
318 | |||
319 | if (is_string($group_definition_value)) { |
||
320 | if ( (is_array($row) && ! array_key_exists($group_definition_value, $row)) |
||
321 | || ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value)) |
||
322 | ) { |
||
323 | throw new UsageException( |
||
324 | 'Unset column for group id generation: ' |
||
325 | .var_export($group_definition_value, true) |
||
326 | ."\n" . var_export($row, true) |
||
327 | ); |
||
328 | } |
||
329 | |||
330 | $part_name .= $group_definition_value; |
||
331 | $group_result_value = $row[ $group_definition_value ]; |
||
332 | } |
||
333 | elseif (is_int($group_definition_value)) { |
||
334 | if ( (is_array($row) && ! array_key_exists($group_definition_value, $row)) |
||
335 | || ($row instanceof \ArrayAcces && ! $row->offsetExists($group_definition_value)) |
||
336 | ) { |
||
337 | throw new UsageException( |
||
338 | 'Unset column for group id generation: ' |
||
339 | .var_export($group_definition_value, true) |
||
340 | ."\n" . var_export($row, true) |
||
341 | ); |
||
342 | } |
||
343 | |||
344 | $part_name .= $group_definition_value ? : '0'; |
||
345 | $group_result_value = $row[ $group_definition_value ]; |
||
346 | } |
||
347 | elseif (is_callable($group_definition_value)) { |
||
348 | |||
349 | if (is_string($group_definition_value)) { |
||
350 | $part_name .= $group_definition_value; |
||
351 | } |
||
352 | // elseif (is_function($value)) { |
||
353 | elseif (is_object($group_definition_value) && ($group_definition_value instanceof \Closure)) { |
||
354 | $part_name .= 'unnamed-closure-' |
||
355 | . hash('crc32b', var_export($group_definition_value, true)); |
||
356 | } |
||
357 | elseif (is_array($group_definition_value)) { |
||
358 | $part_name .= implode('::', $group_definition_value); |
||
359 | } |
||
360 | |||
361 | $group_result_value = call_user_func_array($group_definition_value, [ |
||
362 | $row, &$part_name |
||
363 | ]); |
||
364 | } |
||
365 | else { |
||
366 | throw new UsageException( |
||
367 | 'Bad value provided for group id generation: ' |
||
368 | .var_export($group_definition_value, true) |
||
369 | ."\n" . var_export($row, true) |
||
370 | ); |
||
371 | } |
||
372 | |||
373 | if (!is_null($part_name)) |
||
374 | $group_parts[ $part_name ] = $group_result_value; |
||
375 | } |
||
376 | |||
377 | // sort the groups by names (without it the same group could have multiple ids) |
||
378 | ksort($group_parts); |
||
379 | |||
380 | // bidimensional implode |
||
381 | $out = []; |
||
382 | foreach ($group_parts as $group_name => $group_value) { |
||
383 | if (is_object($group_value)) { |
||
384 | $group_value = get_class($group_value) |
||
385 | . '_' |
||
386 | . hash( 'crc32b', var_export($group_value, true) ); |
||
387 | } |
||
388 | elseif (is_array($group_value)) { |
||
389 | $group_value = 'array_' . hash( 'crc32b', var_export($group_value, true) ); |
||
390 | } |
||
391 | |||
392 | $out[] = $group_name . $key_value_separator . $group_value; |
||
393 | } |
||
394 | |||
395 | return implode($groups_separator, $out); |
||
396 | } |
||
397 | |||
398 | /** |
||
399 | * Search the value associated to the location_parts parameter into the input. |
||
400 | * |
||
401 | * @param array|ArrayAccess $input Typically $_POST|$_GET... |
||
0 ignored issues
–
show
|
|||
402 | * @param scalar|scalar[] $location_parts |
||
403 | * |
||
404 | * @return mixed The value if it exists, null or $default_value otherwize. |
||
405 | * |
||
406 | * Example: |
||
407 | * $array = [ |
||
408 | * 'entry_1' => [ |
||
409 | * 'subentry_1' => 'lolo', |
||
410 | * 'subentry_2' => 'lala', |
||
411 | * ], |
||
412 | * 'entry_2' => [ |
||
413 | * 0 => 'lili', |
||
414 | * 1 => 'lulu', |
||
415 | * ], |
||
416 | * 'entry_3' => 'plop', |
||
417 | * ]; |
||
418 | * |
||
419 | * Arrays::getValueAt($array, ['entry_2', 1]); => 'lulu' |
||
420 | * Arrays::getValueAt($array, ['entry_1', 'subentry_2']); => null |
||
421 | * Arrays::getValueAt($array, ['entry_1', 'subentry_2'], 'lele'); => 'lele' |
||
422 | * Arrays::getValueAt($array, 'entry_3'); => 'plop' |
||
423 | */ |
||
424 | public static function getValueAt($input, $location_parts, $default_value = null) |
||
425 | { |
||
426 | if (is_scalar($location_parts)) { |
||
427 | $location_parts = [$location_parts]; |
||
428 | } |
||
429 | |||
430 | if (! is_array($location_parts)) { |
||
0 ignored issues
–
show
|
|||
431 | throw new \InvalidArgumentException( |
||
432 | "\$location_parts must be a scalar or an array of scalars (as entries of array/ArrayAccess)" |
||
433 | ." instead of ".var_export($location_parts, true) |
||
434 | ); |
||
435 | } |
||
436 | |||
437 | // The value can be inside a multidimension array |
||
438 | $value = $input; |
||
439 | foreach ($location_parts as $location_part) { |
||
440 | if (isset($value[$location_part])) { |
||
441 | $value = $value[$location_part]; |
||
442 | } |
||
443 | else { |
||
444 | return $default_value; |
||
445 | } |
||
446 | } |
||
447 | |||
448 | return $value; |
||
449 | } |
||
450 | |||
451 | /**/ |
||
452 | } |
||
453 |
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
orexit
statements that have been added for debug purposes.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.