1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11: * @link http://cakephp.org CakePHP(tm) Project
12: * @package Cake.Utility
13: * @since CakePHP(tm) v 2.2.0
14: * @license http://www.opensource.org/licenses/mit-license.php MIT License
15: */
16:
17: App::uses('String', 'Utility');
18:
19: /**
20: * Library of array functions for manipulating and extracting data
21: * from arrays or 'sets' of data.
22: *
23: * `Hash` provides an improved interface, more consistent and
24: * predictable set of features over `Set`. While it lacks the spotty
25: * support for pseudo Xpath, its more fully featured dot notation provides
26: * similar features in a more consistent implementation.
27: *
28: * @package Cake.Utility
29: */
30: class Hash {
31:
32: /**
33: * Get a single value specified by $path out of $data.
34: * Does not support the full dot notation feature set,
35: * but is faster for simple read operations.
36: *
37: * @param array $data Array of data to operate on.
38: * @param string|array $path The path being searched for. Either a dot
39: * separated string, or an array of path segments.
40: * @return mixed The value fetched from the array, or null.
41: */
42: public static function get(array $data, $path) {
43: if (empty($data)) {
44: return null;
45: }
46: if (is_string($path) || is_numeric($path)) {
47: $parts = explode('.', $path);
48: } else {
49: $parts = $path;
50: }
51: foreach ($parts as $key) {
52: if (is_array($data) && isset($data[$key])) {
53: $data =& $data[$key];
54: } else {
55: return null;
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 ((array)$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: '/(\[ (?P<attr>[^=><!]+?) (\s* (?P<op>[><!]?[=]|[><]) \s* (?P<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 formatted 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) && isset($return[$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 heterogenous 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: * @param callable $function The function to call on each extracted value.
693: * @return mixed The reduced value.
694: */
695: public static function reduce(array $data, $path, $function) {
696: $values = (array)self::extract($data, $path);
697: return array_reduce($values, $function);
698: }
699:
700: /**
701: * Apply a callback to a set of extracted values using `$function`.
702: * The function will get the extracted values as the first argument.
703: *
704: * ### Example
705: *
706: * You can easily count the results of an extract using apply().
707: * For example to count the comments on an Article:
708: *
709: * `$count = Hash::apply($data, 'Article.Comment.{n}', 'count');`
710: *
711: * You could also use a function like `array_sum` to sum the results.
712: *
713: * `$total = Hash::apply($data, '{n}.Item.price', 'array_sum');`
714: *
715: * @param array $data The data to reduce.
716: * @param string $path The path to extract from $data.
717: * @param callable $function The function to call on each extracted value.
718: * @return mixed The results of the applied method.
719: */
720: public static function apply(array $data, $path, $function) {
721: $values = (array)self::extract($data, $path);
722: return call_user_func($function, $values);
723: }
724:
725: /**
726: * Sorts an array by any value, determined by a Set-compatible path
727: *
728: * ### Sort directions
729: *
730: * - `asc` Sort ascending.
731: * - `desc` Sort descending.
732: *
733: * ## Sort types
734: *
735: * - `regular` For regular sorting (don't change types)
736: * - `numeric` Compare values numerically
737: * - `string` Compare values as strings
738: * - `natural` Compare items as strings using "natural ordering" in a human friendly way.
739: * Will sort foo10 below foo2 as an example. Requires PHP 5.4 or greater or it will fallback to 'regular'
740: *
741: * @param array $data An array of data to sort
742: * @param string $path A Set-compatible path to the array value
743: * @param string $dir See directions above.
744: * @param string $type See direction types above. Defaults to 'regular'.
745: * @return array Sorted array of data
746: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort
747: */
748: public static function sort(array $data, $path, $dir, $type = 'regular') {
749: if (empty($data)) {
750: return array();
751: }
752: $originalKeys = array_keys($data);
753: $numeric = is_numeric(implode('', $originalKeys));
754: if ($numeric) {
755: $data = array_values($data);
756: }
757: $sortValues = self::extract($data, $path);
758: $sortCount = count($sortValues);
759: $dataCount = count($data);
760:
761: // Make sortValues match the data length, as some keys could be missing
762: // the sorted value path.
763: if ($sortCount < $dataCount) {
764: $sortValues = array_pad($sortValues, $dataCount, null);
765: }
766: $result = self::_squash($sortValues);
767: $keys = self::extract($result, '{n}.id');
768: $values = self::extract($result, '{n}.value');
769:
770: $dir = strtolower($dir);
771: $type = strtolower($type);
772: if ($type === 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) {
773: $type = 'regular';
774: }
775: if ($dir === 'asc') {
776: $dir = SORT_ASC;
777: } else {
778: $dir = SORT_DESC;
779: }
780: if ($type === 'numeric') {
781: $type = SORT_NUMERIC;
782: } elseif ($type === 'string') {
783: $type = SORT_STRING;
784: } elseif ($type === 'natural') {
785: $type = SORT_NATURAL;
786: } else {
787: $type = SORT_REGULAR;
788: }
789: array_multisort($values, $dir, $type, $keys, $dir, $type);
790: $sorted = array();
791: $keys = array_unique($keys);
792:
793: foreach ($keys as $k) {
794: if ($numeric) {
795: $sorted[] = $data[$k];
796: continue;
797: }
798: if (isset($originalKeys[$k])) {
799: $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
800: } else {
801: $sorted[$k] = $data[$k];
802: }
803: }
804: return $sorted;
805: }
806:
807: /**
808: * Helper method for sort()
809: * Squashes an array to a single hash so it can be sorted.
810: *
811: * @param array $data The data to squash.
812: * @param string $key The key for the data.
813: * @return array
814: */
815: protected static function _squash($data, $key = null) {
816: $stack = array();
817: foreach ($data as $k => $r) {
818: $id = $k;
819: if ($key !== null) {
820: $id = $key;
821: }
822: if (is_array($r) && !empty($r)) {
823: $stack = array_merge($stack, self::_squash($r, $id));
824: } else {
825: $stack[] = array('id' => $id, 'value' => $r);
826: }
827: }
828: return $stack;
829: }
830:
831: /**
832: * Computes the difference between two complex arrays.
833: * This method differs from the built-in array_diff() in that it will preserve keys
834: * and work on multi-dimensional arrays.
835: *
836: * @param array $data First value
837: * @param array $compare Second value
838: * @return array Returns the key => value pairs that are not common in $data and $compare
839: * The expression for this function is ($data - $compare) + ($compare - ($data - $compare))
840: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff
841: */
842: public static function diff(array $data, $compare) {
843: if (empty($data)) {
844: return (array)$compare;
845: }
846: if (empty($compare)) {
847: return (array)$data;
848: }
849: $intersection = array_intersect_key($data, $compare);
850: while (($key = key($intersection)) !== null) {
851: if ($data[$key] == $compare[$key]) {
852: unset($data[$key]);
853: unset($compare[$key]);
854: }
855: next($intersection);
856: }
857: return $data + $compare;
858: }
859:
860: /**
861: * Merges the difference between $data and $push onto $data.
862: *
863: * @param array $data The data to append onto.
864: * @param array $compare The data to compare and append onto.
865: * @return array The merged array.
866: */
867: public static function mergeDiff(array $data, $compare) {
868: if (empty($data) && !empty($compare)) {
869: return $compare;
870: }
871: if (empty($compare)) {
872: return $data;
873: }
874: foreach ($compare as $key => $value) {
875: if (!array_key_exists($key, $data)) {
876: $data[$key] = $value;
877: } elseif (is_array($value)) {
878: $data[$key] = self::mergeDiff($data[$key], $compare[$key]);
879: }
880: }
881: return $data;
882: }
883:
884: /**
885: * Normalizes an array, and converts it to a standard format.
886: *
887: * @param array $data List to normalize
888: * @param boolean $assoc If true, $data will be converted to an associative array.
889: * @return array
890: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize
891: */
892: public static function normalize(array $data, $assoc = true) {
893: $keys = array_keys($data);
894: $count = count($keys);
895: $numeric = true;
896:
897: if (!$assoc) {
898: for ($i = 0; $i < $count; $i++) {
899: if (!is_int($keys[$i])) {
900: $numeric = false;
901: break;
902: }
903: }
904: }
905: if (!$numeric || $assoc) {
906: $newList = array();
907: for ($i = 0; $i < $count; $i++) {
908: if (is_int($keys[$i])) {
909: $newList[$data[$keys[$i]]] = null;
910: } else {
911: $newList[$keys[$i]] = $data[$keys[$i]];
912: }
913: }
914: $data = $newList;
915: }
916: return $data;
917: }
918:
919: /**
920: * Takes in a flat array and returns a nested array
921: *
922: * ### Options:
923: *
924: * - `children` The key name to use in the resultset for children.
925: * - `idPath` The path to a key that identifies each entry. Should be
926: * compatible with Hash::extract(). Defaults to `{n}.$alias.id`
927: * - `parentPath` The path to a key that identifies the parent of each entry.
928: * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id`
929: * - `root` The id of the desired top-most result.
930: *
931: * @param array $data The data to nest.
932: * @param array $options Options are:
933: * @return array of results, nested
934: * @see Hash::extract()
935: */
936: public static function nest(array $data, $options = array()) {
937: if (!$data) {
938: return $data;
939: }
940:
941: $alias = key(current($data));
942: $options += array(
943: 'idPath' => "{n}.$alias.id",
944: 'parentPath' => "{n}.$alias.parent_id",
945: 'children' => 'children',
946: 'root' => null
947: );
948:
949: $return = $idMap = array();
950: $ids = self::extract($data, $options['idPath']);
951:
952: $idKeys = explode('.', $options['idPath']);
953: array_shift($idKeys);
954:
955: $parentKeys = explode('.', $options['parentPath']);
956: array_shift($parentKeys);
957:
958: foreach ($data as $result) {
959: $result[$options['children']] = array();
960:
961: $id = self::get($result, $idKeys);
962: $parentId = self::get($result, $parentKeys);
963:
964: if (isset($idMap[$id][$options['children']])) {
965: $idMap[$id] = array_merge($result, (array)$idMap[$id]);
966: } else {
967: $idMap[$id] = array_merge($result, array($options['children'] => array()));
968: }
969: if (!$parentId || !in_array($parentId, $ids)) {
970: $return[] =& $idMap[$id];
971: } else {
972: $idMap[$parentId][$options['children']][] =& $idMap[$id];
973: }
974: }
975:
976: if ($options['root']) {
977: $root = $options['root'];
978: } else {
979: $root = self::get($return[0], $parentKeys);
980: }
981:
982: foreach ($return as $i => $result) {
983: $id = self::get($result, $idKeys);
984: $parentId = self::get($result, $parentKeys);
985: if ($id !== $root && $parentId != $root) {
986: unset($return[$i]);
987: }
988: }
989: return array_values($return);
990: }
991:
992: }
993: