INFRA-132 - CRM/Utils - phpcbf
[civicrm-core.git] / CRM / Utils / Array.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26*/
27
28/**
ac302523 29 * Provides a collection of static methods for array manipulation.
6a488035
TO
30 *
31 * @package CRM
06b69b18 32 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
33 */
34class CRM_Utils_Array {
35
36 /**
c9e15d2a
RS
37 * Returns $list[$key] if such element exists, or a default value otherwise.
38 *
39 * If $list is not actually an array at all, then the default value is
40 * returned.
6a488035 41 *
6a488035 42 *
c9e15d2a
RS
43 * @param string $key
44 * Key value to look up in the array.
45 * @param array $list
46 * Array from which to look up a value.
4d63cfde 47 * @param mixed $default
c9e15d2a 48 * (optional) Value to return $list[$key] does not exist.
6a488035 49 *
c9e15d2a
RS
50 * @return mixed
51 * Can return any type, since $list might contain anything.
6a488035 52 */
00be9182 53 public static function value($key, $list, $default = NULL) {
6a488035
TO
54 if (is_array($list)) {
55 return array_key_exists($key, $list) ? $list[$key] : $default;
56 }
57 return $default;
58 }
59
60 /**
c9e15d2a 61 * Recursively searches an array for a key, returning the first value found.
6a488035 62 *
c9e15d2a
RS
63 * If $params[$key] does not exist and $params contains arrays, descend into
64 * each array in a depth-first manner, in array iteration order.
6a488035 65 *
c9e15d2a
RS
66 * @param array $params
67 * The array to be searched.
68 * @param string $key
69 * The key to search for.
70 *
71 * @return mixed
72 * The value of the key, or null if the key is not found.
6a488035 73 */
00be9182 74 public static function retrieveValueRecursive(&$params, $key) {
6a488035
TO
75 if (!is_array($params)) {
76 return NULL;
77 }
78 elseif ($value = CRM_Utils_Array::value($key, $params)) {
79 return $value;
80 }
81 else {
82 foreach ($params as $subParam) {
83 if (is_array($subParam) &&
84 $value = self::retrieveValueRecursive($subParam, $key)
85 ) {
86 return $value;
87 }
88 }
89 }
90 return NULL;
91 }
92
93 /**
c9e15d2a 94 * Wraps and slightly changes the behavior of PHP's array_search().
6a488035 95 *
c9e15d2a
RS
96 * This function reproduces the behavior of array_search() from PHP prior to
97 * version 4.2.0, which was to return NULL on failure. This function also
98 * checks that $list is an array before attempting to search it.
6a488035 99 *
6a488035 100 *
c9e15d2a
RS
101 * @param mixed $value
102 * The value to search for.
103 * @param array $list
104 * The array to be searched.
105 *
106 * @return int|string|null
107 * Returns the key, which could be an int or a string, or NULL on failure.
6a488035 108 */
00be9182 109 public static function key($value, &$list) {
6a488035
TO
110 if (is_array($list)) {
111 $key = array_search($value, $list);
112
113 // array_search returns key if found, false otherwise
114 // it may return values like 0 or empty string which
115 // evaluates to false
116 // hence we must use identical comparison operator
117 return ($key === FALSE) ? NULL : $key;
118 }
119 return NULL;
120 }
121
c9e15d2a
RS
122 /**
123 * Builds an XML fragment representing an array.
124 *
125 * Depending on the nature of the keys of the array (and its sub-arrays,
126 * if any) the XML fragment may not be valid.
127 *
128 * @param array $list
129 * The array to be serialized.
130 * @param int $depth
131 * (optional) Indentation depth counter.
132 * @param string $seperator
133 * (optional) String to be appended after open/close tags.
134 *
c9e15d2a
RS
135 *
136 * @return string
137 * XML fragment representing $list.
138 */
00be9182 139 public static function &xml(&$list, $depth = 1, $seperator = "\n") {
6a488035
TO
140 $xml = '';
141 foreach ($list as $name => $value) {
142 $xml .= str_repeat(' ', $depth * 4);
143 if (is_array($value)) {
144 $xml .= "<{$name}>{$seperator}";
145 $xml .= self::xml($value, $depth + 1, $seperator);
146 $xml .= str_repeat(' ', $depth * 4);
147 $xml .= "</{$name}>{$seperator}";
148 }
149 else {
150 // make sure we escape value
151 $value = self::escapeXML($value);
152 $xml .= "<{$name}>$value</{$name}>{$seperator}";
153 }
154 }
155 return $xml;
156 }
157
c9e15d2a
RS
158 /**
159 * Sanitizes a string for serialization in CRM_Utils_Array::xml().
160 *
161 * Replaces '&', '<', and '>' with their XML escape sequences. Replaces '^A'
162 * with a comma.
163 *
164 * @param string $value
165 * String to be sanitized.
166 *
167 * @return string
168 * Sanitized version of $value.
169 */
00be9182 170 public static function escapeXML($value) {
6a488035
TO
171 static $src = NULL;
172 static $dst = NULL;
173
174 if (!$src) {
175 $src = array('&', '<', '>', '\ 1');
176 $dst = array('&amp;', '&lt;', '&gt;', ',');
177 }
178
179 return str_replace($src, $dst, $value);
180 }
181
182 /**
c9e15d2a
RS
183 * Converts a nested array to a flat array.
184 *
185 * The nested structure is preserved in the string values of the keys of the
186 * flat array.
187 *
188 * Example nested array:
189 * Array
190 * (
191 * [foo] => Array
192 * (
193 * [0] => bar
194 * [1] => baz
195 * [2] => 42
196 * )
f4aaa82a 197 *
c9e15d2a
RS
198 * [asdf] => Array
199 * (
200 * [merp] => bleep
201 * [quack] => Array
202 * (
203 * [0] => 1
204 * [1] => 2
205 * [2] => 3
206 * )
f4aaa82a 207 *
c9e15d2a 208 * )
f4aaa82a 209 *
c9e15d2a
RS
210 * [quux] => 999
211 * )
f4aaa82a 212 *
c9e15d2a
RS
213 * Corresponding flattened array:
214 * Array
215 * (
216 * [foo.0] => bar
217 * [foo.1] => baz
218 * [foo.2] => 42
219 * [asdf.merp] => bleep
220 * [asdf.quack.0] => 1
221 * [asdf.quack.1] => 2
222 * [asdf.quack.2] => 3
223 * [quux] => 999
224 * )
225 *
226 * @param array $list
227 * Array to be flattened.
228 * @param array $flat
229 * Destination array.
6a488035 230 * @param string $prefix
c9e15d2a 231 * (optional) String to prepend to keys.
6a488035 232 * @param string $seperator
c9e15d2a
RS
233 * (optional) String that separates the concatenated keys.
234 *
6a488035 235 */
00be9182 236 public static function flatten(&$list, &$flat, $prefix = '', $seperator = ".") {
6a488035
TO
237 foreach ($list as $name => $value) {
238 $newPrefix = ($prefix) ? $prefix . $seperator . $name : $name;
239 if (is_array($value)) {
240 self::flatten($value, $flat, $newPrefix, $seperator);
241 }
242 else {
243 if (!empty($value)) {
244 $flat[$newPrefix] = $value;
245 }
246 }
247 }
248 }
249
250 /**
c9e15d2a
RS
251 * Converts an array with path-like keys into a tree of arrays.
252 *
253 * This function is the inverse of CRM_Utils_Array::flatten().
254 *
255 * @param string $delim
256 * A path delimiter
257 * @param array $arr
258 * A one-dimensional array indexed by string keys
6a488035 259 *
c9e15d2a
RS
260 * @return array
261 * Array-encoded tree
6a488035 262 *
6a488035 263 */
00be9182 264 public function unflatten($delim, &$arr) {
6a488035
TO
265 $result = array();
266 foreach ($arr as $key => $value) {
267 $path = explode($delim, $key);
268 $node = &$result;
269 while (count($path) > 1) {
270 $key = array_shift($path);
271 if (!isset($node[$key])) {
272 $node[$key] = array();
273 }
274 $node = &$node[$key];
275 }
276 // last part of path
277 $key = array_shift($path);
278 $node[$key] = $value;
279 }
280 return $result;
281 }
282
283 /**
c9e15d2a
RS
284 * Merges two arrays.
285 *
286 * If $a1[foo] and $a2[foo] both exist and are both arrays, the merge
287 * process recurses into those sub-arrays. If $a1[foo] and $a2[foo] both
288 * exist but they are not both arrays, the value from $a1 overrides the
289 * value from $a2 and the value from $a2 is discarded.
6a488035
TO
290 *
291 * @param array $a1
c9e15d2a 292 * First array to be merged.
6a488035 293 * @param array $a2
c9e15d2a 294 * Second array to be merged.
6a488035 295 *
c9e15d2a
RS
296 * @return array
297 * The merged array.
6a488035 298 */
00be9182 299 public static function crmArrayMerge($a1, $a2) {
6a488035
TO
300 if (empty($a1)) {
301 return $a2;
302 }
303
304 if (empty($a2)) {
305 return $a1;
306 }
307
308 $a3 = array();
309 foreach ($a1 as $key => $value) {
310 if (array_key_exists($key, $a2) &&
311 is_array($a2[$key]) && is_array($a1[$key])
312 ) {
313 $a3[$key] = array_merge($a1[$key], $a2[$key]);
314 }
315 else {
316 $a3[$key] = $a1[$key];
317 }
318 }
319
320 foreach ($a2 as $key => $value) {
321 if (array_key_exists($key, $a1)) {
322 // already handled in above loop
323 continue;
324 }
325 $a3[$key] = $a2[$key];
326 }
327
328 return $a3;
329 }
330
c9e15d2a
RS
331 /**
332 * Determines whether an array contains any sub-arrays.
333 *
334 * @param array $list
335 * The array to inspect.
336 *
337 * @return bool
338 * True if $list contains at least one sub-array, false otherwise.
c9e15d2a 339 */
00be9182 340 public static function isHierarchical(&$list) {
6a488035
TO
341 foreach ($list as $n => $v) {
342 if (is_array($v)) {
343 return TRUE;
344 }
345 }
346 return FALSE;
347 }
348
82376c19
TO
349 /**
350 * @param $subset
351 * @param $superset
352 * @return bool TRUE if $subset is a subset of $superset
353 */
00be9182 354 public static function isSubset($subset, $superset) {
82376c19
TO
355 foreach ($subset as $expected) {
356 if (!in_array($expected, $superset)) {
357 return FALSE;
358 }
359 }
360 return TRUE;
361 }
362
6a488035 363 /**
c9e15d2a 364 * Searches an array recursively in an optionally case-insensitive manner.
6a488035 365 *
c9e15d2a
RS
366 * @param string $value
367 * Value to search for.
368 * @param array $params
369 * Array to search within.
370 * @param bool $caseInsensitive
371 * (optional) Whether to search in a case-insensitive manner.
6a488035 372 *
c9e15d2a
RS
373 * @return bool
374 * True if $value was found, false otherwise.
375 *
6a488035 376 */
00be9182 377 public static function crmInArray($value, $params, $caseInsensitive = TRUE) {
6a488035
TO
378 foreach ($params as $item) {
379 if (is_array($item)) {
380 $ret = crmInArray($value, $item, $caseInsensitive);
381 }
382 else {
383 $ret = ($caseInsensitive) ? strtolower($item) == strtolower($value) : $item == $value;
384 if ($ret) {
385 return $ret;
386 }
387 }
388 }
389 return FALSE;
390 }
391
392 /**
393 * This function is used to convert associative array names to values
394 * and vice-versa.
395 *
396 * This function is used by both the web form layer and the api. Note that
397 * the api needs the name => value conversion, also the view layer typically
398 * requires value => name conversion
399 */
00be9182 400 public static function lookupValue(&$defaults, $property, $lookup, $reverse) {
6a488035
TO
401 $id = $property . '_id';
402
403 $src = $reverse ? $property : $id;
404 $dst = $reverse ? $id : $property;
405
406 if (!array_key_exists(strtolower($src), array_change_key_case($defaults, CASE_LOWER))) {
407 return FALSE;
408 }
409
410 $look = $reverse ? array_flip($lookup) : $lookup;
411
412 //trim lookup array, ignore . ( fix for CRM-1514 ), eg for prefix/suffix make sure Dr. and Dr both are valid
413 $newLook = array();
414 foreach ($look as $k => $v) {
415 $newLook[trim($k, ".")] = $v;
416 }
417
418 $look = $newLook;
419
420 if (is_array($look)) {
421 if (!array_key_exists(trim(strtolower($defaults[strtolower($src)]), '.'), array_change_key_case($look, CASE_LOWER))) {
422 return FALSE;
423 }
424 }
425
426 $tempLook = array_change_key_case($look, CASE_LOWER);
427
428 $defaults[$dst] = $tempLook[trim(strtolower($defaults[strtolower($src)]), '.')];
429 return TRUE;
430 }
431
432 /**
c9e15d2a
RS
433 * Checks whether an array is empty.
434 *
435 * An array is empty if its values consist only of NULL and empty sub-arrays.
436 * Containing a non-NULL value or non-empty array makes an array non-empty.
437 *
438 * If something other than an array is passed, it is considered to be empty.
439 *
440 * If nothing is passed at all, the default value provided is empty.
441 *
442 * @param array $array
443 * (optional) Array to be checked for emptiness.
6a488035 444 *
c9e15d2a
RS
445 * @return boolean
446 * True if the array is empty.
6a488035 447 */
00be9182 448 public static function crmIsEmptyArray($array = array()) {
6a488035
TO
449 if (!is_array($array)) {
450 return TRUE;
451 }
452 foreach ($array as $element) {
453 if (is_array($element)) {
454 if (!self::crmIsEmptyArray($element)) {
455 return FALSE;
456 }
457 }
458 elseif (isset($element)) {
459 return FALSE;
460 }
461 }
462 return TRUE;
463 }
464
6a488035 465 /**
c9e15d2a 466 * Sorts an associative array of arrays by an attribute using strnatcmp().
6a488035 467 *
c9e15d2a
RS
468 * @param array $array
469 * Array to be sorted.
470 * @param string $field
471 * Name of the attribute used for sorting.
6a488035 472 *
f4aaa82a 473 * @return array
c9e15d2a 474 * Sorted array
6a488035 475 */
00be9182 476 public static function crmArraySortByField($array, $field) {
6a488035
TO
477 $code = "return strnatcmp(\$a['$field'], \$b['$field']);";
478 uasort($array, create_function('$a,$b', $code));
479 return $array;
480 }
481
482 /**
c9e15d2a 483 * Recursively removes duplicate values from a multi-dimensional array.
6a488035 484 *
c9e15d2a
RS
485 * @param array $array
486 * The input array possibly containing duplicate values.
6a488035 487 *
f4aaa82a 488 * @return array
c9e15d2a 489 * The input array with duplicate values removed.
6a488035 490 */
00be9182 491 public static function crmArrayUnique($array) {
6a488035
TO
492 $result = array_map("unserialize", array_unique(array_map("serialize", $array)));
493 foreach ($result as $key => $value) {
494 if (is_array($value)) {
495 $result[$key] = self::crmArrayUnique($value);
496 }
497 }
498 return $result;
499 }
500
501 /**
c9e15d2a
RS
502 * Sorts an array and maintains index association (with localization).
503 *
504 * Uses Collate from the PECL "intl" package, if available, for UTF-8
505 * sorting (e.g. list of countries). Otherwise calls PHP's asort().
6a488035 506 *
c9e15d2a 507 * On Debian/Ubuntu: apt-get install php5-intl
6a488035 508 *
c9e15d2a
RS
509 * @param array $array
510 * (optional) Array to be sorted.
511 *
f4aaa82a 512 * @return array
c9e15d2a 513 * Sorted array.
6a488035 514 */
00be9182 515 public static function asort($array = array()) {
6a488035
TO
516 $lcMessages = CRM_Utils_System::getUFLocale();
517
518 if ($lcMessages && $lcMessages != 'en_US' && class_exists('Collator')) {
519 $collator = new Collator($lcMessages . '.utf8');
520 $collator->asort($array);
521 }
522 else {
c9e15d2a 523 // This calls PHP's built-in asort().
6a488035
TO
524 asort($array);
525 }
526
527 return $array;
528 }
529
530 /**
c9e15d2a
RS
531 * Unsets an arbitrary list of array elements from an associative array.
532 *
533 * @param array $items
534 * The array from which to remove items.
f4aaa82a
EM
535 *
536 * @internal param string|\string[] $key When passed a string, unsets $items[$key].* When passed a string, unsets $items[$key].
c9e15d2a
RS
537 * When passed an array of strings, unsets $items[$k] for each string $k
538 * in the array.
6a488035 539 */
e7292422
TO
540 public static function remove(&$items) {
541 foreach (func_get_args() as $n => $key) {
542 // Skip argument 0 ($items) by testing $n for truth.
543 if ($n && is_array($key)) {
544 foreach($key as $k) {
545 unset($items[$k]);
546 }
547 }
548 elseif ($n) {
549 unset($items[$key]);
550 }
551 }
552 }
6a488035
TO
553
554 /**
c9e15d2a 555 * Builds an array-tree which indexes the records in an array.
6a488035 556 *
c9e15d2a
RS
557 * @param string[] $keys
558 * Properties by which to index.
559 * @param object|array $records
560 *
561 * @return array
562 * Multi-dimensional array, with one layer for each key.
6a488035 563 */
00be9182 564 public static function index($keys, $records) {
6a488035
TO
565 $final_key = array_pop($keys);
566
567 $result = array();
568 foreach ($records as $record) {
569 $node = &$result;
570 foreach ($keys as $key) {
571 if (is_array($record)) {
928c5201 572 $keyvalue = isset($record[$key]) ? $record[$key] : NULL;
6a488035 573 } else {
928c5201 574 $keyvalue = isset($record->{$key}) ? $record->{$key} : NULL;
6a488035 575 }
17d4d611 576 if (isset($node[$keyvalue]) && !is_array($node[$keyvalue])) {
6a488035
TO
577 $node[$keyvalue] = array();
578 }
579 $node = &$node[$keyvalue];
580 }
581 if (is_array($record)) {
e7292422 582 $node[$record[$final_key]] = $record;
6a488035 583 } else {
e7292422 584 $node[$record->{$final_key}] = $record;
6a488035
TO
585 }
586 }
587 return $result;
588 }
589
590 /**
c9e15d2a 591 * Iterates over a list of records and returns the value of some property.
6a488035
TO
592 *
593 * @param string $prop
c9e15d2a
RS
594 * Property to retrieve.
595 * @param array|object $records
596 * A list of records.
597 *
598 * @return array
599 * Keys are the original keys of $records; values are the $prop values.
6a488035 600 */
00be9182 601 public static function collect($prop, $records) {
6a488035 602 $result = array();
4d34fcfa 603 if (is_array($records)) {
604 foreach ($records as $key => $record) {
605 if (is_object($record)) {
606 $result[$key] = $record->{$prop};
607 } else {
608 $result[$key] = $record[$prop];
609 }
6a488035
TO
610 }
611 }
612 return $result;
613 }
614
6a488035 615 /**
c9e15d2a 616 * Trims delimiters from a string and then splits it using explode().
6a488035 617 *
c9e15d2a
RS
618 * This method works mostly like PHP's built-in explode(), except that
619 * surrounding delimiters are trimmed before explode() is called.
620 *
621 * Also, if an array or NULL is passed as the $values parameter, the value is
622 * returned unmodified rather than being passed to explode().
623 *
624 * @param array|null|string $values
625 * The input string (or an array, or NULL).
6a488035 626 * @param string $delim
c9e15d2a
RS
627 * (optional) The boundary string.
628 *
629 * @return array|null
630 * An array of strings produced by explode(), or the unmodified input
631 * array, or NULL.
6a488035 632 */
00be9182 633 public static function explodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
fe18a93c 634 if ($values === NULL) {
6a488035
TO
635 return NULL;
636 }
fe18a93c
CW
637 // If we already have an array, no need to continue
638 if (is_array($values)) {
639 return $values;
640 }
641 return explode($delim, trim((string) $values, $delim));
6a488035
TO
642 }
643
644 /**
c9e15d2a
RS
645 * Joins array elements with a string, adding surrounding delimiters.
646 *
647 * This method works mostly like PHP's built-in implode(), but the generated
648 * string is surrounded by delimiter characters. Also, if NULL is passed as
649 * the $values parameter, NULL is returned.
6a488035 650 *
fe18a93c 651 * @param mixed $values
c9e15d2a
RS
652 * Array to be imploded. If a non-array is passed, it will be cast to an
653 * array.
6a488035 654 * @param string $delim
c9e15d2a
RS
655 * Delimiter to be used for implode() and which will surround the output
656 * string.
657 *
fe18a93c 658 * @return string|NULL
c9e15d2a 659 * The generated string, or NULL if NULL was passed as $values parameter.
6a488035 660 */
00be9182 661 public static function implodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
6a488035
TO
662 if ($values === NULL) {
663 return NULL;
664 }
fe18a93c
CW
665 // If we already have a string, strip $delim off the ends so it doesn't get added twice
666 if (is_string($values)) {
667 $values = trim($values, $delim);
668 }
669 return $delim . implode($delim, (array) $values) . $delim;
6a488035
TO
670 }
671
672 /**
c9e15d2a
RS
673 * Modifies a key in an array while preserving the key order.
674 *
675 * By default when an element is added to an array, it is added to the end.
676 * This method allows for changing an existing key while preserving its
677 * position in the array.
678 *
679 * The array is both modified in-place and returned.
680 *
681 * @param array $elementArray
682 * Array to manipulate.
683 * @param string $oldKey
684 * Old key to be replaced.
685 * @param string $newKey
686 * Replacement key string.
6a488035 687 *
c9e15d2a
RS
688 * @throws Exception
689 * Throws a generic Exception if $oldKey is not found in $elementArray.
6a488035
TO
690 *
691 * @return array
c9e15d2a 692 * The manipulated array.
6a488035 693 */
00be9182 694 public static function crmReplaceKey(&$elementArray, $oldKey, $newKey) {
6a488035
TO
695 $keys = array_keys($elementArray);
696 if (FALSE === $index = array_search($oldKey, $keys)) {
697 throw new Exception(sprintf('key "%s" does not exit', $oldKey));
698 }
699 $keys[$index] = $newKey;
700 $elementArray = array_combine($keys, array_values($elementArray));
701 return $elementArray;
702 }
bd7b39e7
PJ
703
704 /*
c9e15d2a
RS
705 * Searches array keys by regex, returning the value of the first match.
706 *
707 * Given a regular expression and an array, this method searches the keys
708 * of the array using the regular expression. The first match is then used
709 * to index into the array, and the associated value is retrieved and
710 * returned. If no matches are found, or if something other than an array
711 * is passed, then a default value is returned. Unless otherwise specified,
712 * the default value is NULL.
713 *
714 * @param string $regexKey
715 * The regular expression to use when searching for matching keys.
716 * @param array $list
717 * The array whose keys will be searched.
718 * @param mixed $default
719 * (optional) The default value to return if the regex does not match an
720 * array key, or if something other than an array is passed.
721 *
722 * @return mixed
723 * The value found.
bd7b39e7 724 */
5bc392e6
EM
725 /**
726 * @param $regexKey
727 * @param $list
728 * @param null $default
729 *
730 * @return null
731 */
00be9182 732 public static function valueByRegexKey($regexKey, $list, $default = NULL) {
bd7b39e7
PJ
733 if (is_array($list) && $regexKey) {
734 $matches = preg_grep($regexKey, array_keys($list));
735 $key = reset($matches);
736 return ($key && array_key_exists($key, $list)) ? $list[$key] : $default;
737 }
738 return $default;
739 }
17deafad
TO
740
741 /**
c9e15d2a 742 * Generates the Cartesian product of zero or more vectors.
17deafad 743 *
c9e15d2a
RS
744 * @param array $dimensions
745 * List of dimensions to multiply.
746 * Each key is a dimension name; each value is a vector.
747 * @param array $template
748 * (optional) A base set of values included in every output.
749 *
750 * @return array
751 * Each item is a distinct combination of values from $dimensions.
17deafad
TO
752 *
753 * For example, the product of
754 * {
755 * fg => {red, blue},
756 * bg => {white, black}
757 * }
758 * would be
759 * {
760 * {fg => red, bg => white},
761 * {fg => red, bg => black},
762 * {fg => blue, bg => white},
763 * {fg => blue, bg => black}
764 * }
765 */
00be9182 766 public static function product($dimensions, $template = array()) {
17deafad
TO
767 if (empty($dimensions)) {
768 return array($template);
769 }
770
771 foreach ($dimensions as $key => $value) {
772 $firstKey = $key;
773 $firstValues = $value;
774 break;
775 }
776 unset($dimensions[$key]);
777
778 $results = array();
779 foreach ($firstValues as $firstValue) {
780 foreach (self::product($dimensions, $template) as $result) {
781 $result[$firstKey] = $firstValue;
782 $results[] = $result;
783 }
784 }
785
786 return $results;
787 }
eb20fbf0
TO
788
789 /**
218c8563 790 * Get the first element of an array
eb20fbf0
TO
791 *
792 * @param array $array
793 * @return mixed|NULL
794 */
00be9182 795 public static function first($array) {
eb20fbf0
TO
796 foreach ($array as $value) {
797 return $value;
798 }
799 return NULL;
800 }
768c558c
TO
801
802 /**
803 * Extract any $keys from $array and copy to a new array.
804 *
805 * Note: If a $key does not appear in $array, then it will
806 * not appear in the result.
807 *
808 * @param array $array
77855840
TO
809 * @param array $keys
810 * List of keys to copy.
768c558c
TO
811 * @return array
812 */
00be9182 813 public static function subset($array, $keys) {
768c558c
TO
814 $result = array();
815 foreach ($keys as $key) {
816 if (isset($array[$key])) {
817 $result[$key] = $array[$key];
818 }
819 }
820 return $result;
821 }
a335f6b2 822
b7ceb253
CW
823 /**
824 * Transform an associative array of key=>value pairs into a non-associative array of arrays.
825 * This is necessary to preserve sort order when sending an array through json_encode.
826 *
827 * @param array $associative
828 * @param string $keyName
829 * @param string $valueName
830 * @return array
831 */
00be9182 832 public static function makeNonAssociative($associative, $keyName = 'key', $valueName = 'value') {
b7ceb253
CW
833 $output = array();
834 foreach ($associative as $key => $val) {
835 $output[] = array($keyName => $key, $valueName => $val);
836 }
837 return $output;
838 }
6a488035 839}