Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.28% covered (warning)
69.28%
106 / 153
75.00% covered (warning)
75.00%
15 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
ArrayUtils
69.28% covered (warning)
69.28%
106 / 153
75.00% covered (warning)
75.00%
15 / 20
306.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 unsetArray
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 range
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setArray
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 getArray
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 inArrayRecursive
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 anyInArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 allInArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 arrayToCsv
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 arrayToXml
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getArg
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
9.29
 hasArg
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 arrayFlatten
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 arraySum
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 arraySumRecursive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 abs
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 power
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 sqrt
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 array_diff_assoc_recursive
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
 dot
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
306
 cross3
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Utils
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Utils;
16
17use phpOMS\Math\Matrix\Exception\InvalidDimensionException;
18
19/**
20 * Array utils.
21 *
22 * @package phpOMS\Utils
23 * @license OMS License 2.0
24 * @link    https://jingga.app
25 * @since   1.0.0
26 */
27final class ArrayUtils
28{
29    /**
30     * Constructor.
31     *
32     * @since 1.0.0
33     * @codeCoverageIgnore
34     */
35    private function __construct()
36    {
37    }
38
39    /**
40     * Check if needle exists in multidimensional array.
41     *
42     * @param string $path  Path to element
43     * @param array  $data  Array
44     * @param string $delim Delimiter for path
45     *
46     * @return array
47     *
48     * @throws \Exception
49     *
50     * @since 1.0.0
51     */
52    public static function unsetArray(string $path, array $data, string $delim = '/') : array
53    {
54        $nodes  = \explode($delim, \trim($path, $delim));
55        $prevEl = null;
56        $el     = &$data;
57        $node   = null;
58
59        if ($nodes === false) {
60            throw new \Exception(); // @codeCoverageIgnore
61        }
62
63        foreach ($nodes as $node) {
64            $prevEl = &$el;
65
66            if (!isset($el[$node])) {
67                break;
68            }
69
70            $el = &$el[$node];
71        }
72
73        if ($prevEl !== null) {
74            unset($prevEl[$node]);
75        }
76
77        return $data;
78    }
79
80    /**
81     * Calculate the range of the array
82     *
83     * @param int[]|float[] $values Numeric values
84     *
85     * @return int|float Range of the array
86     *
87     * @since 1.0.0
88     */
89    public static function range(array $values) : int|float
90    {
91        return \max($values) - \min($values);
92    }
93
94    /**
95     * Set element in array by path
96     *
97     * @param string $path      Path to element
98     * @param array  $data      Array
99     * @param mixed  $value     Value to add
100     * @param string $delim     Delimiter for path
101     * @param bool   $overwrite Overwrite if existing
102     *
103     * @return array
104     *
105     * @throws \Exception This exception is thrown if the path is corrupted
106     *
107     * @since 1.0.0
108     */
109    public static function setArray(string $path, array $data, mixed $value, string $delim = '/', bool $overwrite = false) : array
110    {
111        $pathParts = \explode($delim, \trim($path, $delim));
112        $current   = &$data;
113
114        if ($pathParts === false) {
115            throw new \Exception(); // @codeCoverageIgnore
116        }
117
118        foreach ($pathParts as $key) {
119            $current = &$current[$key];
120        }
121
122        if ($overwrite) {
123            $current = $value;
124        } elseif (\is_array($current) && !\is_array($value)) {
125            $current[] = $value;
126        } elseif (\is_array($current) && \is_array($value)) {
127            $current = \array_merge($current, $value);
128        } elseif (\is_scalar($current) && $current !== null) {
129            $current = [$current, $value];
130        } else {
131            $current = $value;
132        }
133
134        return $data;
135    }
136
137    /**
138     * Get element of array by path
139     *
140     * @param string $path  Path to element
141     * @param array  $data  Array
142     * @param string $delim Delimiter for path
143     *
144     * @return mixed
145     *
146     * @throws \Exception This exception is thrown if the path is corrupted
147     *
148     * @since 1.0.0
149     */
150    public static function getArray(string $path, array $data, string $delim = '/') : mixed
151    {
152        $pathParts = \explode($delim, \trim($path, $delim));
153        $current   = $data;
154
155        if ($pathParts === false) {
156            throw new \Exception(); // @codeCoverageIgnore
157        }
158
159        foreach ($pathParts as $key) {
160            if (!isset($current[$key])) {
161                return null;
162            }
163
164            $current = $current[$key];
165        }
166
167        return $current;
168    }
169
170    /**
171     * Check if needle exists in multidimensional array.
172     *
173     * @param mixed $needle   Needle for search
174     * @param array $haystack Haystack for search
175     * @param mixed $key      Key that has to match (optional)
176     *
177     * @return bool
178     *
179     * @since 1.0.0
180     */
181    public static function inArrayRecursive(mixed $needle, array $haystack, $key = null) : bool
182    {
183        $found = false;
184
185        foreach ($haystack as $k => $item) {
186            if ($item === $needle && ($key === null || $key === $k)) {
187                return true;
188            } elseif (\is_array($item)) {
189                $found = self::inArrayRecursive($needle, $item, $key);
190
191                if ($found) {
192                    return true;
193                }
194            }
195        }
196
197        return $found;
198    }
199
200    /**
201     * Check if any of the needles are in the array
202     *
203     * @param array $needles  Needles for search
204     * @param array $haystack Haystack for search
205     *
206     * @return bool
207     *
208     * @since 1.0.0
209     */
210    public static function anyInArray(array $needles, array $haystack) : bool
211    {
212        foreach ($needles as $needle) {
213            if (\in_array($needle, $haystack)) {
214                return true;
215            }
216        }
217
218        return false;
219    }
220
221    /**
222     * Check if all of the needles are in the array
223     *
224     * @param array $needles  Needles for search
225     * @param array $haystack Haystack for search
226     *
227     * @return bool
228     *
229     * @since 1.0.0
230     */
231    public static function allInArray(array $needles, array $haystack) : bool
232    {
233        foreach ($needles as $needle) {
234            if (!\in_array($needle, $haystack)) {
235                return false;
236            }
237        }
238
239        return true;
240    }
241
242    /**
243     * Convert array to csv string.
244     *
245     * @param array  $data      Data to convert
246     * @param string $delimiter Delim to use
247     * @param string $enclosure Enclosure to use
248     * @param string $escape    Escape to use
249     *
250     * @return string
251     *
252     * @since 1.0.0
253     */
254    public static function arrayToCsv(array $data, string $delimiter = ';', string $enclosure = '"', string $escape = '\\') : string
255    {
256        $outstream = \fopen('php://memory', 'r+');
257
258        if ($outstream === false) {
259            throw new \Exception(); // @codeCoverageIgnore
260        }
261
262        foreach ($data as $line) {
263            /** @noinspection PhpMethodParametersCountMismatchInspection */
264            \fputcsv($outstream, $line, $delimiter, $enclosure, $escape);
265        }
266
267        \rewind($outstream);
268        $csv = \stream_get_contents($outstream);
269        \fclose($outstream);
270
271        return $csv === false ? '' : $csv;
272    }
273
274    /**
275     * Convert array to xml string.
276     *
277     * @param array             $data Data to convert
278     * @param \SimpleXMLElement $xml  XML parent
279     *
280     * @return string
281     *
282     * @since 1.0.0
283     */
284    public static function arrayToXml(array $data, \SimpleXMLElement $xml = null) : string
285    {
286        $xml ??= new \SimpleXMLElement('<root/>');
287
288        foreach ($data as $key => $value) {
289            if (\is_array($value)) {
290                self::arrayToXml($value, $xml->addChild($key));
291            } else {
292                $xml->addChild($key, \htmlspecialchars($value));
293            }
294        }
295        return (string) $xml->asXML();
296    }
297
298    /**
299     * Get array value by argument id.
300     *
301     * Useful for parsing command line parsing
302     *
303     * @template T
304     *
305     * @param string               $id   Id to find
306     * @param array<string|int, T> $args CLI command list
307     *
308     * @return mixed
309     *
310     * @since 1.0.0
311     */
312    public static function getArg(string $id, array $args) : mixed
313    {
314        $key = 0;
315        if (\is_numeric($id)) {
316            $key = ((int) $id) - 1;
317        } else {
318            if (($key = \array_search($id, $args)) === false || $key === \count($args) - 1) {
319                return null;
320            }
321
322            $key  = (int) $key;
323            $args = \array_values($args);
324        }
325
326        $value = $args[$key + 1] ?? null;
327
328        return \is_string($value) ? \trim($value, '\'" ') : $value;
329    }
330
331    /**
332     * Check if flag is set
333     *
334     * @param string                   $id   Id to find
335     * @param array<string|int, mixed> $args CLI command list
336     *
337     * @return int
338     *
339     * @since 1.0.0
340     */
341    public static function hasArg(string $id, array $args) : int
342    {
343        return ($key = \array_search($id, $args)) === false
344            ? -1
345            : (int) $key;
346    }
347
348    /**
349     * Flatten array
350     *
351     * Reduces multi dimensional array to one dimensional array. Flatten tries to maintain the index as far as possible.
352     *
353     * @param array $array Multi dimensional array to flatten
354     *
355     * @return array
356     *
357     * @since 1.0.0
358     */
359    public static function arrayFlatten(array $array) : array
360    {
361        // see collection collapse as alternative?!
362        $flat  = [];
363        $stack = \array_values($array);
364
365        while (!empty($stack)) {
366            $value = \array_shift($stack);
367
368            if (\is_array($value)) {
369                $stack = \array_merge(\array_values($value), $stack);
370            } else {
371                $flat[] = $value;
372            }
373        }
374
375        return $flat;
376    }
377
378    /**
379     * Sum of array elements
380     *
381     * @param array $array Array to sum
382     * @param int   $start Start index
383     * @param int   $count Amount of elements to sum
384     *
385     * @return int|float
386     *
387     * @since 1.0.0
388     */
389    public static function arraySum(array $array, int $start = 0, int $count = 0) : int | float
390    {
391        $count = $count === 0 ? \count($array) : $start + $count;
392        $sum   = 0;
393        $array = \array_values($array);
394
395        for ($i = $start; $i <= $count - 1; ++$i) {
396            $sum += $array[$i];
397        }
398
399        return $sum;
400    }
401
402    /**
403     * Sum multi dimensional array
404     *
405     * @param array $array Multi dimensional array to flatten
406     *
407     * @return mixed
408     *
409     * @since 1.0.0
410     */
411    public static function arraySumRecursive(array $array) : mixed
412    {
413        return \array_sum(self::arrayFlatten($array));
414    }
415
416    /**
417     * Applying abs to every array value
418     *
419     * @param array<int|float> $values Numeric values
420     *
421     * @return array<int|float>
422     *
423     * @since 1.0.0
424     */
425    public static function abs(array $values) : array
426    {
427        $abs = [];
428
429        foreach ($values as $value) {
430            $abs[] = \abs($value);
431        }
432
433        return $abs;
434    }
435
436    /**
437     * Power all values in array.
438     *
439     * @param array<int|float> $values Values to square
440     * @param float            $exp    Exponent
441     *
442     * @return float[]
443     *
444     * @since 1.0.0
445     */
446    public static function power(array $values, int | float $exp = 2) : array
447    {
448        $squared = [];
449
450        foreach ($values as $value) {
451            $squared[] = $value ** $exp;
452        }
453
454        return $squared;
455    }
456
457    /**
458     * Sqrt all values in array.
459     *
460     * @param array<int|float> $values Values to sqrt
461     *
462     * @return array<int|float>
463     *
464     * @since 1.0.0
465     */
466    public static function sqrt(array $values) : array
467    {
468        $squared = [];
469
470        foreach ($values as $value) {
471            $squared[] = \sqrt($value);
472        }
473
474        return $squared;
475    }
476
477    /**
478     * Get the associative difference of two arrays.
479     *
480     * @param array $values1 Array 1
481     * @param array $values2 Array 2
482     *
483     * @return array
484     *
485     * @since 1.0.0
486     */
487    public static function array_diff_assoc_recursive(array $values1, array $values2) : array
488    {
489        $diff = [];
490        foreach ($values1 as $key => $value) {
491            if (\is_array($value)) {
492                if (!\array_key_exists($key, $values2) || !\is_array($values2[$key])) {
493                    $diff[$key] = $value;
494                } else {
495                    $subDiff = self::array_diff_assoc_recursive($value, $values2[$key]);
496                    if (!empty($subDiff)) {
497                        $diff[$key] = $subDiff;
498                    }
499                }
500            } elseif (!\array_key_exists($key, $values2) || $values2[$key] !== $value) {
501                $diff[$key] = $value;
502            }
503        }
504
505        return $diff;
506    }
507
508    /**
509     * Get the dot product of two arrays
510     *
511     * @param array $value1 Value 1 is a matrix or a vector
512     * @param array $value2 Value 2 is a matrix or vector (cannot be a matrix if value1 is a vector)
513     *
514     * @return array
515     *
516     * @throws InvalidDimensionException
517     * @throws \InvalidArgumentException
518     *
519     * @since 1.0.0
520     */
521    public static function dot(array $value1, array $value2) : int|float|array
522    {
523        $m1 = \count($value1);
524        $n1 = ($isMatrix1 = \is_array($value1[0])) ? \count($value1[0]) : 1;
525
526        $m2 = \count($value2);
527        $n2 = ($isMatrix2 = \is_array($value2[0])) ? \count($value2[0]) : 1;
528
529        $result = null;
530
531        if ($isMatrix1 && $isMatrix2) {
532            if ($m2 !== $n1) {
533                throw new InvalidDimensionException($m2 . 'x' . $n2 . ' not compatible with ' . $m1 . 'x' . $n1);
534            }
535
536            $result = [[]];
537            for ($i = 0; $i < $m1; ++$i) { // Row of 1
538                for ($c = 0; $c < $n2; ++$c) { // Column of 2
539                    $temp = 0;
540
541                    for ($j = 0; $j < $m2; ++$j) { // Row of 2
542                        $temp += $value1[$i][$j] * $value2[$j][$c];
543                    }
544
545                    $result[$i][$c] = $temp;
546                }
547            }
548        } elseif (!$isMatrix1 && !$isMatrix2) {
549            if ($m1 !== $m2) {
550                throw new InvalidDimensionException($m1 . ' vs. ' . $m2);
551            }
552
553            $result = 0;
554            for ($i = 0; $i < $m1; ++$i) {
555                $result += $value1[$i] * $value2[$i];
556            }
557        } elseif ($isMatrix1 && !$isMatrix2) {
558            $result = [];
559            for ($i = 0; $i < $m1; ++$i) { // Row of 1
560                $temp = 0;
561
562                for ($c = 0; $c < $m2; ++$c) { // Row of 2
563                    $temp += $value1[$i][$c] * $value2[$c];
564                }
565
566                $result[$i] = $temp;
567            }
568        } else {
569            throw new \InvalidArgumentException();
570        }
571
572        return $result;
573    }
574
575    /**
576     * Calculate the vector corss product
577     *
578     * @param array $vector1 First 3 vector
579     * @param array $vector2 Second 3 vector
580     *
581     * @return array<int, int|float>
582     *
583     * @since 1.0.0
584     */
585    public function cross3(array $vector1, array $vector2) : array
586    {
587        return [
588            $vector1[1] * $vector2[2] - $vector1[2] * $vector2[1],
589            $vector1[2] * $vector2[0] - $vector1[0] * $vector2[2],
590            $vector1[0] * $vector2[1] - $vector1[1] * $vector2[0],
591        ];
592    }
593}