Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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