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