統計
| ブランチ: | リビジョン:

pictcode / lib / Cake / Utility / Hash.php @ 635eef61

履歴 | 表示 | アノテート | ダウンロード (31.303 KB)

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 null;
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
}