Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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