1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4: * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * Redistributions of files must retain the above copyright notice.
8: *
9: * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
10: * @link http://cakephp.org CakePHP(tm) Project
11: * @package Cake.Utility
12: * @since CakePHP(tm) v 2.2.0
13: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
14: */
15:
16: App::uses('String', 'Utility');
17:
18: /**
19: * Library of array functions for manipulating and extracting data
20: * from arrays or 'sets' of data.
21: *
22: * `Hash` provides an improved interface, more consistent and
23: * predictable set of features over `Set`. While it lacks the spotty
24: * support for pseudo Xpath, its more fully featured dot notation provides
25: * similar features in a more consistent implementation.
26: *
27: * @package Cake.Utility
28: */
29: class Hash {
30:
31: /**
32: * Get a single value specified by $path out of $data.
33: * Does not support the full dot notation feature set,
34: * but is faster for simple read operations.
35: *
36: * @param array $data Array of data to operate on.
37: * @param string|array $path The path being searched for. Either a dot
38: * separated string, or an array of path segments.
39: * @return mixed The value fetched from the array, or null.
40: */
41: public static function get(array $data, $path) {
42: if (empty($data)) {
43: return null;
44: }
45: if (is_string($path) || is_numeric($path)) {
46: $parts = explode('.', $path);
47: } else {
48: $parts = $path;
49: }
50: foreach ($parts as $key) {
51: if (is_array($data) && isset($data[$key])) {
52: $data =& $data[$key];
53: } else {
54: return null;
55: }
56:
57: }
58: return $data;
59: }
60:
61: /**
62: * Gets the values from an array matching the $path expression.
63: * The path expression is a dot separated expression, that can contain a set
64: * of patterns and expressions:
65: *
66: * - `{n}` Matches any numeric key, or integer.
67: * - `{s}` Matches any string key.
68: * - `Foo` Matches any key with the exact same value.
69: *
70: * There are a number of attribute operators:
71: *
72: * - `=`, `!=` Equality.
73: * - `>`, `<`, `>=`, `<=` Value comparison.
74: * - `=/.../` Regular expression pattern match.
75: *
76: * Given a set of User array data, from a `$User->find('all')` call:
77: *
78: * - `1.User.name` Get the name of the user at index 1.
79: * - `{n}.User.name` Get the name of every user in the set of users.
80: * - `{n}.User[id]` Get the name of every user with an id key.
81: * - `{n}.User[id>=2]` Get the name of every user with an id key greater than or equal to 2.
82: * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`.
83: *
84: * @param array $data The data to extract from.
85: * @param string $path The path to extract.
86: * @return array An array of the extracted values. Returns an empty array
87: * if there are no matches.
88: */
89: public static function extract(array $data, $path) {
90: if (empty($path)) {
91: return $data;
92: }
93:
94: // Simple paths.
95: if (!preg_match('/[{\[]/', $path)) {
96: return (array)self::get($data, $path);
97: }
98:
99: if (strpos($path, '[') === false) {
100: $tokens = explode('.', $path);
101: } else {
102: $tokens = String::tokenize($path, '.', '[', ']');
103: }
104:
105: $_key = '__set_item__';
106:
107: $context = array($_key => array($data));
108:
109: foreach ($tokens as $token) {
110: $next = array();
111:
112: $conditions = false;
113: $position = strpos($token, '[');
114: if ($position !== false) {
115: $conditions = substr($token, $position);
116: $token = substr($token, 0, $position);
117: }
118:
119: foreach ($context[$_key] as $item) {
120: foreach ($item as $k => $v) {
121: if (self::_matchToken($k, $token)) {
122: $next[] = $v;
123: }
124: }
125: }
126:
127: // Filter for attributes.
128: if ($conditions) {
129: $filter = array();
130: foreach ($next as $item) {
131: if (self::_matches($item, $conditions)) {
132: $filter[] = $item;
133: }
134: }
135: $next = $filter;
136: }
137: $context = array($_key => $next);
138:
139: }
140: return $context[$_key];
141: }
142:
143: /**
144: * Check a key against a token.
145: *
146: * @param string $key The key in the array being searched.
147: * @param string $token The token being matched.
148: * @return boolean
149: */
150: protected static function _matchToken($key, $token) {
151: if ($token === '{n}') {
152: return is_numeric($key);
153: }
154: if ($token === '{s}') {
155: return is_string($key);
156: }
157: if (is_numeric($token)) {
158: return ($key == $token);
159: }
160: return ($key === $token);
161: }
162:
163: /**
164: * Checks whether or not $data matches the attribute patterns
165: *
166: * @param array $data Array of data to match.
167: * @param string $selector The patterns to match.
168: * @return boolean Fitness of expression.
169: */
170: protected static function _matches(array $data, $selector) {
171: preg_match_all(
172: '/(\[ (?<attr>[^=><!]+?) (\s* (?<op>[><!]?[=]|[><]) \s* (?<val>[^\]]+) )? \])/x',
173: $selector,
174: $conditions,
175: PREG_SET_ORDER
176: );
177:
178: foreach ($conditions as $cond) {
179: $attr = $cond['attr'];
180: $op = isset($cond['op']) ? $cond['op'] : null;
181: $val = isset($cond['val']) ? $cond['val'] : null;
182:
183: // Presence test.
184: if (empty($op) && empty($val) && !isset($data[$attr])) {
185: return false;
186: }
187:
188: // Empty attribute = fail.
189: if (!(isset($data[$attr]) || array_key_exists($attr, $data))) {
190: return false;
191: }
192:
193: $prop = isset($data[$attr]) ? $data[$attr] : null;
194:
195: // Pattern matches and other operators.
196: if ($op === '=' && $val && $val[0] === '/') {
197: if (!preg_match($val, $prop)) {
198: return false;
199: }
200: } elseif (
201: ($op === '=' && $prop != $val) ||
202: ($op === '!=' && $prop == $val) ||
203: ($op === '>' && $prop <= $val) ||
204: ($op === '<' && $prop >= $val) ||
205: ($op === '>=' && $prop < $val) ||
206: ($op === '<=' && $prop > $val)
207: ) {
208: return false;
209: }
210:
211: }
212: return true;
213: }
214:
215: /**
216: * Insert $values into an array with the given $path. You can use
217: * `{n}` and `{s}` elements to insert $data multiple times.
218: *
219: * @param array $data The data to insert into.
220: * @param string $path The path to insert at.
221: * @param array $values The values to insert.
222: * @return array The data with $values inserted.
223: */
224: public static function insert(array $data, $path, $values = null) {
225: $tokens = explode('.', $path);
226: if (strpos($path, '{') === false) {
227: return self::_simpleOp('insert', $data, $tokens, $values);
228: }
229:
230: $token = array_shift($tokens);
231: $nextPath = implode('.', $tokens);
232: foreach ($data as $k => $v) {
233: if (self::_matchToken($k, $token)) {
234: $data[$k] = self::insert($v, $nextPath, $values);
235: }
236: }
237: return $data;
238: }
239:
240: /**
241: * Perform a simple insert/remove operation.
242: *
243: * @param string $op The operation to do.
244: * @param array $data The data to operate on.
245: * @param array $path The path to work on.
246: * @param mixed $values The values to insert when doing inserts.
247: * @return array $data.
248: */
249: protected static function _simpleOp($op, $data, $path, $values = null) {
250: $_list =& $data;
251:
252: $count = count($path);
253: $last = $count - 1;
254: foreach ($path as $i => $key) {
255: if (is_numeric($key) && intval($key) > 0 || $key === '0') {
256: $key = intval($key);
257: }
258: if ($op === 'insert') {
259: if ($i === $last) {
260: $_list[$key] = $values;
261: return $data;
262: }
263: if (!isset($_list[$key])) {
264: $_list[$key] = array();
265: }
266: $_list =& $_list[$key];
267: if (!is_array($_list)) {
268: $_list = array();
269: }
270: } elseif ($op === 'remove') {
271: if ($i === $last) {
272: unset($_list[$key]);
273: return $data;
274: }
275: if (!isset($_list[$key])) {
276: return $data;
277: }
278: $_list =& $_list[$key];
279: }
280: }
281: }
282:
283: /**
284: * Remove data matching $path from the $data array.
285: * You can use `{n}` and `{s}` to remove multiple elements
286: * from $data.
287: *
288: * @param array $data The data to operate on
289: * @param string $path A path expression to use to remove.
290: * @return array The modified array.
291: */
292: public static function remove(array $data, $path) {
293: $tokens = explode('.', $path);
294: if (strpos($path, '{') === false) {
295: return self::_simpleOp('remove', $data, $tokens);
296: }
297:
298: $token = array_shift($tokens);
299: $nextPath = implode('.', $tokens);
300: foreach ($data as $k => $v) {
301: $match = self::_matchToken($k, $token);
302: if ($match && is_array($v)) {
303: $data[$k] = self::remove($v, $nextPath);
304: } elseif ($match) {
305: unset($data[$k]);
306: }
307: }
308: return $data;
309: }
310:
311: /**
312: * Creates an associative array using `$keyPath` as the path to build its keys, and optionally
313: * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized
314: * to null (useful for Hash::merge). You can optionally group the values by what is obtained when
315: * following the path specified in `$groupPath`.
316: *
317: * @param array $data Array from where to extract keys and values
318: * @param string $keyPath A dot-separated string.
319: * @param string $valuePath A dot-separated string.
320: * @param string $groupPath A dot-separated string.
321: * @return array Combined array
322: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::combine
323: */
324: public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) {
325: if (empty($data)) {
326: return array();
327: }
328:
329: if (is_array($keyPath)) {
330: $format = array_shift($keyPath);
331: $keys = self::format($data, $keyPath, $format);
332: } else {
333: $keys = self::extract($data, $keyPath);
334: }
335: if (empty($keys)) {
336: return array();
337: }
338:
339: if (!empty($valuePath) && is_array($valuePath)) {
340: $format = array_shift($valuePath);
341: $vals = self::format($data, $valuePath, $format);
342: } elseif (!empty($valuePath)) {
343: $vals = self::extract($data, $valuePath);
344: }
345:
346: $count = count($keys);
347: for ($i = 0; $i < $count; $i++) {
348: $vals[$i] = isset($vals[$i]) ? $vals[$i] : null;
349: }
350:
351: if ($groupPath !== null) {
352: $group = self::extract($data, $groupPath);
353: if (!empty($group)) {
354: $c = count($keys);
355: for ($i = 0; $i < $c; $i++) {
356: if (!isset($group[$i])) {
357: $group[$i] = 0;
358: }
359: if (!isset($out[$group[$i]])) {
360: $out[$group[$i]] = array();
361: }
362: $out[$group[$i]][$keys[$i]] = $vals[$i];
363: }
364: return $out;
365: }
366: }
367: if (empty($vals)) {
368: return array();
369: }
370: return array_combine($keys, $vals);
371: }
372:
373: /**
374: * Returns a formated series of values extracted from `$data`, using
375: * `$format` as the format and `$paths` as the values to extract.
376: *
377: * Usage:
378: *
379: * {{{
380: * $result = Hash::format($users, array('{n}.User.id', '{n}.User.name'), '%s : %s');
381: * }}}
382: *
383: * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do.
384: *
385: * @param array $data Source array from which to extract the data
386: * @param string $paths An array containing one or more Hash::extract()-style key paths
387: * @param string $format Format string into which values will be inserted, see sprintf()
388: * @return array An array of strings extracted from `$path` and formatted with `$format`
389: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format
390: * @see sprintf()
391: * @see Hash::extract()
392: */
393: public static function format(array $data, array $paths, $format) {
394: $extracted = array();
395: $count = count($paths);
396:
397: if (!$count) {
398: return;
399: }
400:
401: for ($i = 0; $i < $count; $i++) {
402: $extracted[] = self::extract($data, $paths[$i]);
403: }
404: $out = array();
405: $data = $extracted;
406: $count = count($data[0]);
407:
408: $countTwo = count($data);
409: for ($j = 0; $j < $count; $j++) {
410: $args = array();
411: for ($i = 0; $i < $countTwo; $i++) {
412: if (array_key_exists($j, $data[$i])) {
413: $args[] = $data[$i][$j];
414: }
415: }
416: $out[] = vsprintf($format, $args);
417: }
418: return $out;
419: }
420:
421: /**
422: * Determines if one array contains the exact keys and values of another.
423: *
424: * @param array $data The data to search through.
425: * @param array $needle The values to file in $data
426: * @return boolean true if $data contains $needle, false otherwise
427: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::contains
428: */
429: public static function contains(array $data, array $needle) {
430: if (empty($data) || empty($needle)) {
431: return false;
432: }
433: $stack = array();
434:
435: while (!empty($needle)) {
436: $key = key($needle);
437: $val = $needle[$key];
438: unset($needle[$key]);
439:
440: if (isset($data[$key]) && is_array($val)) {
441: $next = $data[$key];
442: unset($data[$key]);
443:
444: if (!empty($val)) {
445: $stack[] = array($val, $next);
446: }
447: } elseif (!isset($data[$key]) || $data[$key] != $val) {
448: return false;
449: }
450:
451: if (empty($needle) && !empty($stack)) {
452: list($needle, $data) = array_pop($stack);
453: }
454: }
455: return true;
456: }
457:
458: /**
459: * Test whether or not a given path exists in $data.
460: * This method uses the same path syntax as Hash::extract()
461: *
462: * Checking for paths that could target more than one element will
463: * make sure that at least one matching element exists.
464: *
465: * @param array $data The data to check.
466: * @param string $path The path to check for.
467: * @return boolean Existence of path.
468: * @see Hash::extract()
469: */
470: public static function check(array $data, $path) {
471: $results = self::extract($data, $path);
472: if (!is_array($results)) {
473: return false;
474: }
475: return count($results) > 0;
476: }
477:
478: /**
479: * Recursively filters a data set.
480: *
481: * @param array $data Either an array to filter, or value when in callback
482: * @param callable $callback A function to filter the data with. Defaults to
483: * `self::_filter()` Which strips out all non-zero empty values.
484: * @return array Filtered array
485: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::filter
486: */
487: public static function filter(array $data, $callback = array('self', '_filter')) {
488: foreach ($data as $k => $v) {
489: if (is_array($v)) {
490: $data[$k] = self::filter($v, $callback);
491: }
492: }
493: return array_filter($data, $callback);
494: }
495:
496: /**
497: * Callback function for filtering.
498: *
499: * @param array $var Array to filter.
500: * @return boolean
501: */
502: protected static function _filter($var) {
503: if ($var === 0 || $var === '0' || !empty($var)) {
504: return true;
505: }
506: return false;
507: }
508:
509: /**
510: * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
511: * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
512: * array('0.Foo.Bar' => 'Far').)
513: *
514: * @param array $data Array to flatten
515: * @param string $separator String used to separate array key elements in a path, defaults to '.'
516: * @return array
517: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::flatten
518: */
519: public static function flatten(array $data, $separator = '.') {
520: $result = array();
521: $stack = array();
522: $path = null;
523:
524: reset($data);
525: while (!empty($data)) {
526: $key = key($data);
527: $element = $data[$key];
528: unset($data[$key]);
529:
530: if (is_array($element) && !empty($element)) {
531: if (!empty($data)) {
532: $stack[] = array($data, $path);
533: }
534: $data = $element;
535: reset($data);
536: $path .= $key . $separator;
537: } else {
538: $result[$path . $key] = $element;
539: }
540:
541: if (empty($data) && !empty($stack)) {
542: list($data, $path) = array_pop($stack);
543: reset($data);
544: }
545: }
546: return $result;
547: }
548:
549: /**
550: * Expand/unflattens an string to an array
551: *
552: * For example, unflattens an array that was collapsed with `Hash::flatten()`
553: * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes
554: * `array(array('Foo' => array('Bar' => 'Far')))`.
555: *
556: * @param array $data Flattened array
557: * @param string $separator The delimiter used
558: * @return array
559: */
560: public static function expand($data, $separator = '.') {
561: $result = array();
562: foreach ($data as $flat => $value) {
563: $keys = explode($separator, $flat);
564: $keys = array_reverse($keys);
565: $child = array(
566: $keys[0] => $value
567: );
568: array_shift($keys);
569: foreach ($keys as $k) {
570: $child = array(
571: $k => $child
572: );
573: }
574: $result = self::merge($result, $child);
575: }
576: return $result;
577: }
578:
579: /**
580: * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`.
581: *
582: * The difference between this method and the built-in ones, is that if an array key contains another array, then
583: * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for
584: * keys that contain scalar values (unlike `array_merge_recursive`).
585: *
586: * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays.
587: *
588: * @param array $data Array to be merged
589: * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged
590: * @return array Merged array
591: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge
592: */
593: public static function merge(array $data, $merge) {
594: $args = func_get_args();
595: $return = current($args);
596:
597: while (($arg = next($args)) !== false) {
598: foreach ((array)$arg as $key => $val) {
599: if (!empty($return[$key]) && is_array($return[$key]) && is_array($val)) {
600: $return[$key] = self::merge($return[$key], $val);
601: } elseif (is_int($key)) {
602: $return[] = $val;
603: } else {
604: $return[$key] = $val;
605: }
606: }
607: }
608: return $return;
609: }
610:
611: /**
612: * Checks to see if all the values in the array are numeric
613: *
614: * @param array $array The array to check.
615: * @return boolean true if values are numeric, false otherwise
616: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::numeric
617: */
618: public static function numeric(array $data) {
619: if (empty($data)) {
620: return false;
621: }
622: $values = array_values($data);
623: $str = implode('', $values);
624: return (bool)ctype_digit($str);
625: }
626:
627: /**
628: * Counts the dimensions of an array.
629: * Only considers the dimension of the first element in the array.
630: *
631: * If you have an un-even or hetrogenous array, consider using Hash::maxDimensions()
632: * to get the dimensions of the array.
633: *
634: * @param array $array Array to count dimensions on
635: * @return integer The number of dimensions in $data
636: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::dimensions
637: */
638: public static function dimensions(array $data) {
639: if (empty($data)) {
640: return 0;
641: }
642: reset($data);
643: $depth = 1;
644: while ($elem = array_shift($data)) {
645: if (is_array($elem)) {
646: $depth += 1;
647: $data =& $elem;
648: } else {
649: break;
650: }
651: }
652: return $depth;
653: }
654:
655: /**
656: * Counts the dimensions of *all* array elements. Useful for finding the maximum
657: * number of dimensions in a mixed array.
658: *
659: * @param array $data Array to count dimensions on
660: * @return integer The maximum number of dimensions in $data
661: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::maxDimensions
662: */
663: public static function maxDimensions(array $data) {
664: $depth = array();
665: if (is_array($data) && reset($data) !== false) {
666: foreach ($data as $value) {
667: $depth[] = self::dimensions((array)$value) + 1;
668: }
669: }
670: return max($depth);
671: }
672:
673: /**
674: * Map a callback across all elements in a set.
675: * Can be provided a path to only modify slices of the set.
676: *
677: * @param array $data The data to map over, and extract data out of.
678: * @param string $path The path to extract for mapping over.
679: * @param callable $function The function to call on each extracted value.
680: * @return array An array of the modified values.
681: */
682: public static function map(array $data, $path, $function) {
683: $values = (array)self::extract($data, $path);
684: return array_map($function, $values);
685: }
686:
687: /**
688: * Reduce a set of extracted values using `$function`.
689: *
690: * @param array $data The data to reduce.
691: * @param string $path The path to extract from $data.
692: * @return mixed The reduced value.
693: */
694: public static function reduce(array $data, $path, $function) {
695: $values = (array)self::extract($data, $path);
696: return array_reduce($values, $function);
697: }
698:
699: /**
700: * Apply a callback to a set of extracted values using `$function`.
701: * The function will get the extracted values as the first argument.
702: *
703: * ### Example
704: *
705: * You can easily count the results of an extract using apply().
706: * For example to count the comments on an Article:
707: *
708: * `$count = Hash::apply($data, 'Article.Comment.{n}', 'count');`
709: *
710: * You could also use a function like `array_sum` to sum the results.
711: *
712: * `$total = Hash::apply($data, '{n}.Item.price', 'array_sum');`
713: *
714: * @param array $data The data to reduce.
715: * @param string $path The path to extract from $data.
716: * @return mixed The results of the applied method.
717: */
718: public static function apply(array $data, $path, $function) {
719: $values = (array)self::extract($data, $path);
720: return call_user_func($function, $values);
721: }
722:
723: /**
724: * Sorts an array by any value, determined by a Set-compatible path
725: *
726: * ### Sort directions
727: *
728: * - `asc` Sort ascending.
729: * - `desc` Sort descending.
730: *
731: * ## Sort types
732: *
733: * - `numeric` Sort by numeric value.
734: * - `regular` Sort by numeric value.
735: * - `string` Sort by numeric value.
736: * - `natural` Sort by natural order. Requires PHP 5.4 or greater.
737: *
738: * @param array $data An array of data to sort
739: * @param string $path A Set-compatible path to the array value
740: * @param string $dir See directions above.
741: * @param string $type See direction types above. Defaults to 'regular'.
742: * @return array Sorted array of data
743: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort
744: */
745: public static function sort(array $data, $path, $dir, $type = 'regular') {
746: if (empty($data)) {
747: return array();
748: }
749: $originalKeys = array_keys($data);
750: $numeric = is_numeric(implode('', $originalKeys));
751: if ($numeric) {
752: $data = array_values($data);
753: }
754: $sortValues = self::extract($data, $path);
755: $sortCount = count($sortValues);
756: $dataCount = count($data);
757:
758: // Make sortValues match the data length, as some keys could be missing
759: // the sorted value path.
760: if ($sortCount < $dataCount) {
761: $sortValues = array_pad($sortValues, $dataCount, null);
762: }
763: $result = self::_squash($sortValues);
764: $keys = self::extract($result, '{n}.id');
765: $values = self::extract($result, '{n}.value');
766:
767: $dir = strtolower($dir);
768: $type = strtolower($type);
769: if ($type == 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) {
770: $type == 'regular';
771: }
772: if ($dir === 'asc') {
773: $dir = SORT_ASC;
774: } else {
775: $dir = SORT_DESC;
776: }
777: if ($type === 'numeric') {
778: $type = SORT_NUMERIC;
779: } elseif ($type === 'string') {
780: $type = SORT_STRING;
781: } elseif ($type === 'natural') {
782: $type = SORT_NATURAL;
783: } else {
784: $type = SORT_REGULAR;
785: }
786: array_multisort($values, $dir, $type, $keys, $dir, $type);
787: $sorted = array();
788: $keys = array_unique($keys);
789:
790: foreach ($keys as $k) {
791: if ($numeric) {
792: $sorted[] = $data[$k];
793: continue;
794: }
795: if (isset($originalKeys[$k])) {
796: $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
797: } else {
798: $sorted[$k] = $data[$k];
799: }
800: }
801: return $sorted;
802: }
803:
804: /**
805: * Helper method for sort()
806: * Sqaushes an array to a single hash so it can be sorted.
807: *
808: * @param array $data The data to squash.
809: * @param string $key The key for the data.
810: * @return array
811: */
812: protected static function _squash($data, $key = null) {
813: $stack = array();
814: foreach ($data as $k => $r) {
815: $id = $k;
816: if (!is_null($key)) {
817: $id = $key;
818: }
819: if (is_array($r) && !empty($r)) {
820: $stack = array_merge($stack, self::_squash($r, $id));
821: } else {
822: $stack[] = array('id' => $id, 'value' => $r);
823: }
824: }
825: return $stack;
826: }
827:
828: /**
829: * Computes the difference between two complex arrays.
830: * This method differs from the built-in array_diff() in that it will preserve keys
831: * and work on multi-dimensional arrays.
832: *
833: * @param array $data First value
834: * @param array $compare Second value
835: * @return array Returns the key => value pairs that are not common in $data and $compare
836: * The expression for this function is ($data - $compare) + ($compare - ($data - $compare))
837: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff
838: */
839: public static function diff(array $data, $compare) {
840: if (empty($data)) {
841: return (array)$compare;
842: }
843: if (empty($compare)) {
844: return (array)$data;
845: }
846: $intersection = array_intersect_key($data, $compare);
847: while (($key = key($intersection)) !== null) {
848: if ($data[$key] == $compare[$key]) {
849: unset($data[$key]);
850: unset($compare[$key]);
851: }
852: next($intersection);
853: }
854: return $data + $compare;
855: }
856:
857: /**
858: * Merges the difference between $data and $push onto $data.
859: *
860: * @param array $data The data to append onto.
861: * @param array $compare The data to compare and append onto.
862: * @return array The merged array.
863: */
864: public static function mergeDiff(array $data, $compare) {
865: if (empty($data) && !empty($compare)) {
866: return $compare;
867: }
868: if (empty($compare)) {
869: return $data;
870: }
871: foreach ($compare as $key => $value) {
872: if (!array_key_exists($key, $data)) {
873: $data[$key] = $value;
874: } elseif (is_array($value)) {
875: $data[$key] = self::mergeDiff($data[$key], $compare[$key]);
876: }
877: }
878: return $data;
879: }
880:
881: /**
882: * Normalizes an array, and converts it to a standard format.
883: *
884: * @param array $data List to normalize
885: * @param boolean $assoc If true, $data will be converted to an associative array.
886: * @return array
887: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize
888: */
889: public static function normalize(array $data, $assoc = true) {
890: $keys = array_keys($data);
891: $count = count($keys);
892: $numeric = true;
893:
894: if (!$assoc) {
895: for ($i = 0; $i < $count; $i++) {
896: if (!is_int($keys[$i])) {
897: $numeric = false;
898: break;
899: }
900: }
901: }
902: if (!$numeric || $assoc) {
903: $newList = array();
904: for ($i = 0; $i < $count; $i++) {
905: if (is_int($keys[$i])) {
906: $newList[$data[$keys[$i]]] = null;
907: } else {
908: $newList[$keys[$i]] = $data[$keys[$i]];
909: }
910: }
911: $data = $newList;
912: }
913: return $data;
914: }
915:
916: /**
917: * Takes in a flat array and returns a nested array
918: *
919: * ### Options:
920: *
921: * - `children` The key name to use in the resultset for children.
922: * - `idPath` The path to a key that identifies each entry. Should be
923: * compatible with Hash::extract(). Defaults to `{n}.$alias.id`
924: * - `parentPath` The path to a key that identifies the parent of each entry.
925: * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id`
926: * - `root` The id of the desired top-most result.
927: *
928: * @param array $data The data to nest.
929: * @param array $options Options are:
930: * @return array of results, nested
931: * @see Hash::extract()
932: */
933: public static function nest(array $data, $options = array()) {
934: if (!$data) {
935: return $data;
936: }
937:
938: $alias = key(current($data));
939: $options += array(
940: 'idPath' => "{n}.$alias.id",
941: 'parentPath' => "{n}.$alias.parent_id",
942: 'children' => 'children',
943: 'root' => null
944: );
945:
946: $return = $idMap = array();
947: $ids = self::extract($data, $options['idPath']);
948:
949: $idKeys = explode('.', $options['idPath']);
950: array_shift($idKeys);
951:
952: $parentKeys = explode('.', $options['parentPath']);
953: array_shift($parentKeys);
954:
955: foreach ($data as $result) {
956: $result[$options['children']] = array();
957:
958: $id = self::get($result, $idKeys);
959: $parentId = self::get($result, $parentKeys);
960:
961: if (isset($idMap[$id][$options['children']])) {
962: $idMap[$id] = array_merge($result, (array)$idMap[$id]);
963: } else {
964: $idMap[$id] = array_merge($result, array($options['children'] => array()));
965: }
966: if (!$parentId || !in_array($parentId, $ids)) {
967: $return[] =& $idMap[$id];
968: } else {
969: $idMap[$parentId][$options['children']][] =& $idMap[$id];
970: }
971: }
972:
973: if ($options['root']) {
974: $root = $options['root'];
975: } else {
976: $root = self::get($return[0], $parentKeys);
977: }
978:
979: foreach ($return as $i => $result) {
980: $id = self::get($result, $idKeys);
981: $parentId = self::get($result, $parentKeys);
982: if ($id !== $root && $parentId != $root) {
983: unset($return[$i]);
984: }
985: }
986: return array_values($return);
987: }
988:
989: }
990: