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