575c0e66b880594f254cbab8145633d0c5ebe319
[civicrm-core.git] / CRM / Utils / Array.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2013
32 * $Id$
33 *
34 */
35 class CRM_Utils_Array {
36
37 /**
38 * if the key exists in the list returns the associated value
39 *
40 * @access public
41 *
42 * @param string $key the key value
43 * @param array $list the array to be searched
44 * @param mixed $default
45 *
46 * @return mixed value if exists else $default
47 * @static
48 */
49 static function value($key, $list, $default = NULL) {
50 if (is_array($list)) {
51 return array_key_exists($key, $list) ? $list[$key] : $default;
52 }
53 return $default;
54 }
55
56 /**
57 * Given a parameter array and a key to search for,
58 * search recursively for that key's value.
59 *
60 * @param array $values The parameter array
61 * @param string $key The key to search for
62 *
63 * @return mixed The value of the key, or null.
64 * @access public
65 * @static
66 */
67 static function retrieveValueRecursive(&$params, $key) {
68 if (!is_array($params)) {
69 return NULL;
70 }
71 elseif ($value = CRM_Utils_Array::value($key, $params)) {
72 return $value;
73 }
74 else {
75 foreach ($params as $subParam) {
76 if (is_array($subParam) &&
77 $value = self::retrieveValueRecursive($subParam, $key)
78 ) {
79 return $value;
80 }
81 }
82 }
83 return NULL;
84 }
85
86 /**
87 * if the value exists in the list returns the associated key
88 *
89 * @access public
90 *
91 * @param list the array to be searched
92 * @param value the search value
93 *
94 * @return key if exists else null
95 * @static
96 * @access public
97 *
98 */
99 static function key($value, &$list) {
100 if (is_array($list)) {
101 $key = array_search($value, $list);
102
103 // array_search returns key if found, false otherwise
104 // it may return values like 0 or empty string which
105 // evaluates to false
106 // hence we must use identical comparison operator
107 return ($key === FALSE) ? NULL : $key;
108 }
109 return NULL;
110 }
111
112 static function &xml(&$list, $depth = 1, $seperator = "\n") {
113 $xml = '';
114 foreach ($list as $name => $value) {
115 $xml .= str_repeat(' ', $depth * 4);
116 if (is_array($value)) {
117 $xml .= "<{$name}>{$seperator}";
118 $xml .= self::xml($value, $depth + 1, $seperator);
119 $xml .= str_repeat(' ', $depth * 4);
120 $xml .= "</{$name}>{$seperator}";
121 }
122 else {
123 // make sure we escape value
124 $value = self::escapeXML($value);
125 $xml .= "<{$name}>$value</{$name}>{$seperator}";
126 }
127 }
128 return $xml;
129 }
130
131 static function escapeXML($value) {
132 static $src = NULL;
133 static $dst = NULL;
134
135 if (!$src) {
136 $src = array('&', '<', '>', '\ 1');
137 $dst = array('&amp;', '&lt;', '&gt;', ',');
138 }
139
140 return str_replace($src, $dst, $value);
141 }
142
143 /**
144 * Convert an array-tree to a flat array
145 *
146 * @param array $list the original, tree-shaped list
147 * @param array $flat the flat list to which items will be copied
148 * @param string $prefix
149 * @param string $seperator
150 */
151 static function flatten(&$list, &$flat, $prefix = '', $seperator = ".") {
152 foreach ($list as $name => $value) {
153 $newPrefix = ($prefix) ? $prefix . $seperator . $name : $name;
154 if (is_array($value)) {
155 self::flatten($value, $flat, $newPrefix, $seperator);
156 }
157 else {
158 if (!empty($value)) {
159 $flat[$newPrefix] = $value;
160 }
161 }
162 }
163 }
164
165 /**
166 * Convert an array with path-like keys into a tree of arrays
167 *
168 * @param $delim A path delimiter
169 * @param $arr A one-dimensional array indexed by string keys
170 *
171 * @return array-encoded tree
172 */
173 function unflatten($delim, &$arr) {
174 $result = array();
175 foreach ($arr as $key => $value) {
176 $path = explode($delim, $key);
177 $node = &$result;
178 while (count($path) > 1) {
179 $key = array_shift($path);
180 if (!isset($node[$key])) {
181 $node[$key] = array();
182 }
183 $node = &$node[$key];
184 }
185 // last part of path
186 $key = array_shift($path);
187 $node[$key] = $value;
188 }
189 return $result;
190 }
191
192 /**
193 * Funtion to merge to two arrays recursively
194 *
195 * @param array $a1
196 * @param array $a2
197 *
198 * @return $a3
199 * @static
200 */
201 static function crmArrayMerge($a1, $a2) {
202 if (empty($a1)) {
203 return $a2;
204 }
205
206 if (empty($a2)) {
207 return $a1;
208 }
209
210 $a3 = array();
211 foreach ($a1 as $key => $value) {
212 if (array_key_exists($key, $a2) &&
213 is_array($a2[$key]) && is_array($a1[$key])
214 ) {
215 $a3[$key] = array_merge($a1[$key], $a2[$key]);
216 }
217 else {
218 $a3[$key] = $a1[$key];
219 }
220 }
221
222 foreach ($a2 as $key => $value) {
223 if (array_key_exists($key, $a1)) {
224 // already handled in above loop
225 continue;
226 }
227 $a3[$key] = $a2[$key];
228 }
229
230 return $a3;
231 }
232
233 static function isHierarchical(&$list) {
234 foreach ($list as $n => $v) {
235 if (is_array($v)) {
236 return TRUE;
237 }
238 }
239 return FALSE;
240 }
241
242 /**
243 * Array deep copy
244 *
245 * @params array $array
246 * @params int $maxdepth
247 * @params int $depth
248 *
249 * @return array copy of the array
250 *
251 * @static
252 * @access public
253 */
254 static function array_deep_copy(&$array, $maxdepth = 50, $depth = 0) {
255 if ($depth > $maxdepth) {
256 return $array;
257 }
258 $copy = array();
259 foreach ($array as $key => $value) {
260 if (is_array($value)) {
261 array_deep_copy($value, $copy[$key], $maxdepth, ++$depth);
262 }
263 else {
264 $copy[$key] = $value;
265 }
266 }
267 return $copy;
268 }
269
270 /**
271 * In some cases, functions return an array by reference, but we really don't
272 * want to receive a reference.
273 *
274 * @param $array
275 * @return mixed
276 */
277 static function breakReference($array) {
278 $copy = $array;
279 return $copy;
280 }
281
282 /**
283 * Array splice function that preserves associative keys
284 * defauly php array_splice function doesnot preserve keys
285 * So specify start and end of the array that you want to remove
286 *
287 * @param array $params array to slice
288 * @param Integer $start
289 * @param Integer $end
290 *
291 * @return void
292 * @static
293 */
294 static function crmArraySplice(&$params, $start, $end) {
295 // verify start and end date
296 if ($start < 0) {
297 $start = 0;
298 }
299 if ($end > count($params)) {
300 $end = count($params);
301 }
302
303 $i = 0;
304
305 // procees unset operation
306 foreach ($params as $key => $value) {
307 if ($i >= $start && $i < $end) {
308 unset($params[$key]);
309 }
310 $i++;
311 }
312 }
313
314 /**
315 * Function for case insensitive in_array search
316 *
317 * @param $value value or search string
318 * @param $params array that need to be searched
319 * @param $caseInsensitive boolean true or false
320 *
321 * @static
322 */
323 static function crmInArray($value, $params, $caseInsensitive = TRUE) {
324 foreach ($params as $item) {
325 if (is_array($item)) {
326 $ret = crmInArray($value, $item, $caseInsensitive);
327 }
328 else {
329 $ret = ($caseInsensitive) ? strtolower($item) == strtolower($value) : $item == $value;
330 if ($ret) {
331 return $ret;
332 }
333 }
334 }
335 return FALSE;
336 }
337
338 /**
339 * This function is used to convert associative array names to values
340 * and vice-versa.
341 *
342 * This function is used by both the web form layer and the api. Note that
343 * the api needs the name => value conversion, also the view layer typically
344 * requires value => name conversion
345 */
346 static function lookupValue(&$defaults, $property, $lookup, $reverse) {
347 $id = $property . '_id';
348
349 $src = $reverse ? $property : $id;
350 $dst = $reverse ? $id : $property;
351
352 if (!array_key_exists(strtolower($src), array_change_key_case($defaults, CASE_LOWER))) {
353 return FALSE;
354 }
355
356 $look = $reverse ? array_flip($lookup) : $lookup;
357
358 //trim lookup array, ignore . ( fix for CRM-1514 ), eg for prefix/suffix make sure Dr. and Dr both are valid
359 $newLook = array();
360 foreach ($look as $k => $v) {
361 $newLook[trim($k, ".")] = $v;
362 }
363
364 $look = $newLook;
365
366 if (is_array($look)) {
367 if (!array_key_exists(trim(strtolower($defaults[strtolower($src)]), '.'), array_change_key_case($look, CASE_LOWER))) {
368 return FALSE;
369 }
370 }
371
372 $tempLook = array_change_key_case($look, CASE_LOWER);
373
374 $defaults[$dst] = $tempLook[trim(strtolower($defaults[strtolower($src)]), '.')];
375 return TRUE;
376 }
377
378 /**
379 * Function to check if give array is empty
380 * @param array $array array to check for empty condition
381 *
382 * @return boolean true is array is empty else false
383 * @static
384 */
385 static function crmIsEmptyArray($array = array()) {
386 if (!is_array($array)) {
387 return TRUE;
388 }
389 foreach ($array as $element) {
390 if (is_array($element)) {
391 if (!self::crmIsEmptyArray($element)) {
392 return FALSE;
393 }
394 }
395 elseif (isset($element)) {
396 return FALSE;
397 }
398 }
399 return TRUE;
400 }
401
402 /**
403 * Function to determine how many levels in array for multidimensional arrays
404 *
405 * @param array $array
406 *
407 * @return integer $levels containing number of levels in array
408 * @static
409 */
410 static function getLevelsArray($array) {
411 if (!is_array($array)) {
412 return 0;
413 }
414 $jsonString = json_encode($array);
415 $parts = explode("}", $jsonString);
416 $max = 0;
417 foreach ($parts as $part) {
418 $countLevels = substr_count($part, "{");
419 if ($countLevels > $max) {
420 $max = $countLevels;
421 }
422 }
423 return $max;
424 }
425
426 /**
427 * Function to sort an associative array of arrays by an attribute using natural string compare
428 *
429 * @param array $array Array to be sorted
430 * @param string $field Name of the attribute you want to sort by
431 *
432 * @return array $array Sorted array
433 * @static
434 */
435 static function crmArraySortByField($array, $field) {
436 $code = "return strnatcmp(\$a['$field'], \$b['$field']);";
437 uasort($array, create_function('$a,$b', $code));
438 return $array;
439 }
440
441 /**
442 * Recursively removes duplicate values from an multi-dimensional array.
443 *
444 * @param array $array The input array possibly containing duplicate values.
445 *
446 * @return array $array The array with duplicate values removed.
447 * @static
448 */
449 static function crmArrayUnique($array) {
450 $result = array_map("unserialize", array_unique(array_map("serialize", $array)));
451 foreach ($result as $key => $value) {
452 if (is_array($value)) {
453 $result[$key] = self::crmArrayUnique($value);
454 }
455 }
456 return $result;
457 }
458
459 /**
460 * Sort an array and maintain index association, use Collate from the
461 * PECL "intl" package, if available, for UTF-8 sorting (ex: list of countries).
462 * On Debian/Ubuntu: apt-get install php5-intl
463 *
464 * @param array $array array of values
465 *
466 * @return array Sorted array
467 * @static
468 */
469 static function asort($array = array()) {
470 $lcMessages = CRM_Utils_System::getUFLocale();
471
472 if ($lcMessages && $lcMessages != 'en_US' && class_exists('Collator')) {
473 $collator = new Collator($lcMessages . '.utf8');
474 $collator->asort($array);
475 }
476 else {
477 asort($array);
478 }
479
480 return $array;
481 }
482
483 /**
484 * Convenient way to unset a bunch of items from an array
485 *
486 * @param array $items (reference)
487 * @param string/int/array $itemN: other params to this function will be treated as keys
488 * (or arrays of keys) to unset
489 */
490 static function remove(&$items) {
491 foreach (func_get_args() as $n => $key) {
492 if ($n && is_array($key)) {
493 foreach($key as $k) {
494 unset($items[$k]);
495 }
496 }
497 elseif ($n) {
498 unset($items[$key]);
499 }
500 }
501 }
502
503 /**
504 * Build an array-tree which indexes the records in an array
505 *
506 * @param $keys array of string (properties by which to index)
507 * @param $records array of records (objects or assoc-arrays)
508 * @return array; multi-dimensional, with one layer for each key
509 */
510 static function index($keys, $records) {
511 $final_key = array_pop($keys);
512
513 $result = array();
514 foreach ($records as $record) {
515 $node = &$result;
516 foreach ($keys as $key) {
517 if (is_array($record)) {
518 $keyvalue = $record[$key];
519 } else {
520 $keyvalue = $record->{$key};
521 }
522 if (isset($node[$keyvalue]) && !is_array($node[$keyvalue])) {
523 $node[$keyvalue] = array();
524 }
525 $node = &$node[$keyvalue];
526 }
527 if (is_array($record)) {
528 $node[ $record[$final_key] ] = $record;
529 } else {
530 $node[ $record->{$final_key} ] = $record;
531 }
532 }
533 return $result;
534 }
535
536 /**
537 * Iterate through a list of records and grab the value of some property
538 *
539 * @param string $prop
540 * @param array $records a list of records (object|array)
541 * @return array keys are the original keys of $records; values are the $prop values
542 */
543 static function collect($prop, $records) {
544 $result = array();
545 foreach ($records as $key => $record) {
546 if (is_object($record)) {
547 $result[$key] = $record->{$prop};
548 } else {
549 $result[$key] = $record[$prop];
550 }
551 }
552 return $result;
553 }
554
555 /**
556 * Given a list of key-value pairs, combine thme into a single string
557 * @param array $pairs e.g. array('a' => '1', 'b' => '2')
558 * @param string $l1Delim e.g. ','
559 * @param string $l2Delim e.g. '='
560 * @return string e.g. 'a=1,b=2'
561 */
562 static function implodeKeyValue($l1Delim, $l2Delim, $pairs) {
563 $exprs = array();
564 foreach ($pairs as $key => $value) {
565 $exprs[] = $key . $l2Delim . $value;
566 }
567 return implode($l1Delim, $exprs);
568 }
569
570 /**
571 * Like explode() but assumes that the $value is padded with $delim on left and right
572 *
573 * @param mixed $values
574 * @param string $delim
575 * @return array|NULL
576 */
577 static function explodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
578 if ($values === NULL) {
579 return NULL;
580 }
581 // If we already have an array, no need to continue
582 if (is_array($values)) {
583 return $values;
584 }
585 return explode($delim, trim((string) $values, $delim));
586 }
587
588 /**
589 * Like implode() but creates a string that is padded with $delim on left and right
590 *
591 * @param mixed $values
592 * @param string $delim
593 * @return string|NULL
594 */
595 static function implodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
596 if ($values === NULL) {
597 return NULL;
598 }
599 // If we already have a string, strip $delim off the ends so it doesn't get added twice
600 if (is_string($values)) {
601 $values = trim($values, $delim);
602 }
603 return $delim . implode($delim, (array) $values) . $delim;
604 }
605
606 /**
607 * Function to modify the key in an array without actually changing the order
608 * By default when you add an element it is added at the end
609 *
610 * @param array $elementArray associated array element
611 * @param string $oldKey old key
612 * @param string $newKey new key
613 *
614 * @return array
615 */
616 static function crmReplaceKey(&$elementArray, $oldKey, $newKey) {
617 $keys = array_keys($elementArray);
618 if (FALSE === $index = array_search($oldKey, $keys)) {
619 throw new Exception(sprintf('key "%s" does not exit', $oldKey));
620 }
621 $keys[$index] = $newKey;
622 $elementArray = array_combine($keys, array_values($elementArray));
623 return $elementArray;
624 }
625
626 /*
627 * function to get value of first matched
628 * regex key element of an array
629 */
630 static function valueByRegexKey($regexKey, $list, $default = NULL) {
631 if (is_array($list) && $regexKey) {
632 $matches = preg_grep($regexKey, array_keys($list));
633 $key = reset($matches);
634 return ($key && array_key_exists($key, $list)) ? $list[$key] : $default;
635 }
636 return $default;
637 }
638
639 /**
640 * Generate the Cartesian product of zero or more vectors
641 *
642 * @param array $dimensions list of dimensions to multiply; each key is a dimension name; each value is a vector
643 * @param array $template a base set of values included in every output
644 * @return array each item is a distinct combination of values from $dimensions
645 *
646 * For example, the product of
647 * {
648 * fg => {red, blue},
649 * bg => {white, black}
650 * }
651 * would be
652 * {
653 * {fg => red, bg => white},
654 * {fg => red, bg => black},
655 * {fg => blue, bg => white},
656 * {fg => blue, bg => black}
657 * }
658 */
659 static function product($dimensions, $template = array()) {
660 if (empty($dimensions)) {
661 return array($template);
662 }
663
664 foreach ($dimensions as $key => $value) {
665 $firstKey = $key;
666 $firstValues = $value;
667 break;
668 }
669 unset($dimensions[$key]);
670
671 $results = array();
672 foreach ($firstValues as $firstValue) {
673 foreach (self::product($dimensions, $template) as $result) {
674 $result[$firstKey] = $firstValue;
675 $results[] = $result;
676 }
677 }
678
679 return $results;
680 }
681 }
682