Subversion-Projekte lars-tiefland.cakephp

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/* SVN FILE: $Id: set.php 8004 2009-01-16 20:15:21Z gwoo $ */
3
/**
4
 * Library of array functions for Cake.
5
 *
6
 * PHP versions 4 and 5
7
 *
8
 * CakePHP(tm) :  Rapid Development Framework (http://www.cakephp.org)
9
 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
10
 *
11
 * Licensed under The MIT License
12
 * Redistributions of files must retain the above copyright notice.
13
 *
14
 * @filesource
15
 * @copyright     Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
16
 * @link          http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
17
 * @package       cake
18
 * @subpackage    cake.cake.libs
19
 * @since         CakePHP(tm) v 1.2.0
20
 * @version       $Revision: 8004 $
21
 * @modifiedby    $LastChangedBy: gwoo $
22
 * @lastmodified  $Date: 2009-01-16 12:15:21 -0800 (Fri, 16 Jan 2009) $
23
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
24
 */
25
/**
26
 * Class used for manipulation of arrays.
27
 *
28
 * Long description for class
29
 *
30
 * @package       cake
31
 * @subpackage    cake.cake.libs
32
 */
33
class Set extends Object {
34
/**
35
 * Deprecated
36
 *
37
 */
38
	var $value = array();
39
/**
40
 * This function can be thought of as a hybrid between PHP's array_merge and array_merge_recursive. The difference
41
 * to the two is that if an array key contains another array then the function behaves recursive (unlike array_merge)
42
 * but does not do if for keys containing strings (unlike array_merge_recursive). See the unit test for more information.
43
 *
44
 * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays.
45
 *
46
 * @param array $arr1 Array to be merged
47
 * @param array $arr2 Array to merge with
48
 * @return array Merged array
49
 * @access public
50
 * @static
51
 */
52
	function merge($arr1, $arr2 = null) {
53
		$args = func_get_args();
54
 
55
		$r = (array)current($args);
56
		while (($arg = next($args)) !== false) {
57
			foreach ((array)$arg as $key => $val)	 {
58
				if (is_array($val) && isset($r[$key]) && is_array($r[$key])) {
59
					$r[$key] = Set::merge($r[$key], $val);
60
				} elseif (is_int($key)) {
61
					$r[] = $val;
62
				} else {
63
					$r[$key] = $val;
64
				}
65
			}
66
		}
67
		return $r;
68
	}
69
/**
70
 * Filters empty elements out of a route array, excluding '0'.
71
 *
72
 * @param mixed $var Either an array to filter, or value when in callback
73
 * @param boolean $isArray Force to tell $var is an array when $var is empty
74
 * @return mixed Either filtered array, or true/false when in callback
75
 * @access public
76
 * @static
77
 */
78
	function filter($var, $isArray = false) {
79
		if (is_array($var) && (!empty($var) || $isArray)) {
80
			return array_filter($var, array('Set', 'filter'));
81
		}
82
 
83
		if ($var === 0 || $var === '0' || !empty($var)) {
84
			return true;
85
		}
86
		return false;
87
	}
88
/**
89
 * Pushes the differences in $array2 onto the end of $array
90
 *
91
 * @param mixed $array Original array
92
 * @param mixed $array2 Differences to push
93
 * @return array Combined array
94
 * @access public
95
 * @static
96
 */
97
	function pushDiff($array, $array2) {
98
		if (empty($array) && !empty($array2)) {
99
			return $array2;
100
		}
101
		if (!empty($array) && !empty($array2)) {
102
			foreach ($array2 as $key => $value) {
103
				if (!array_key_exists($key, $array)) {
104
					$array[$key] = $value;
105
				} else {
106
					if (is_array($value)) {
107
						$array[$key] = Set::pushDiff($array[$key], $array2[$key]);
108
					}
109
				}
110
			}
111
		}
112
		return $array;
113
	}
114
/**
115
 * Maps the contents of the Set object to an object hierarchy.
116
 * Maintains numeric keys as arrays of objects
117
 *
118
 * @param string $class A class name of the type of object to map to
119
 * @param string $tmp A temporary class name used as $class if $class is an array
120
 * @return object Hierarchical object
121
 * @access public
122
 * @static
123
 */
124
	function map($class = 'stdClass', $tmp = 'stdClass') {
125
		if (is_array($class)) {
126
			$val = $class;
127
			$class = $tmp;
128
		}
129
 
130
		if (empty($val)) {
131
			return null;
132
		}
133
		return Set::__map($val, $class);
134
	}
135
 
136
/**
137
 * Get the array value of $array. If $array is null, it will return
138
 * the current array Set holds. If it is an object of type Set, it
139
 * will return its value. If it is another object, its object variables.
140
 * If it is anything else but an array, it will return an array whose first
141
 * element is $array.
142
 *
143
 * @param mixed $array Data from where to get the array.
144
 * @return array Array from $array.
145
 * @access private
146
 */
147
	function __array($array) {
148
		if (empty($array)) {
149
			$array = array();
150
		} elseif (is_object($array)) {
151
			$array = get_object_vars($array);
152
		} elseif (!is_array($array)) {
153
			$array = array($array);
154
		}
155
		return $array;
156
	}
157
 
158
/**
159
 * Maps the given value as an object. If $value is an object,
160
 * it returns $value. Otherwise it maps $value as an object of
161
 * type $class, and if primary assign _name_ $key on first array.
162
 * If $value is not empty, it will be used to set properties of
163
 * returned object (recursively). If $key is numeric will maintain array
164
 * structure
165
 *
166
 * @param mixed $value Value to map
167
 * @param string $class Class name
168
 * @param boolean $primary whether to assign first array key as the _name_
169
 * @return mixed Mapped object
170
 * @access private
171
 * @static
172
 */
173
	function __map(&$array, $class, $primary = false) {
174
		if ($class === true) {
175
			$out = new stdClass;
176
		} else {
177
			$out = new $class;
178
		}
179
		if (is_array($array)) {
180
			$keys = array_keys($array);
181
			foreach ($array as $key => $value) {
182
				if ($keys[0] === $key && $class !== true) {
183
					$primary = true;
184
				}
185
				if (is_numeric($key)) {
186
					if (is_object($out)) {
187
						$out = get_object_vars($out);
188
					}
189
					$out[$key] = Set::__map($value, $class);
190
					if (is_object($out[$key])) {
191
						if ($primary !== true && is_array($value) && Set::countDim($value, true) === 2) {
192
							if (!isset($out[$key]->_name_)) {
193
								$out[$key]->_name_ = $primary;
194
							}
195
						}
196
					}
197
				} elseif (is_array($value)) {
198
					if ($primary === true) {
199
						if (!isset($out->_name_)) {
200
							$out->_name_ = $key;
201
						}
202
						$primary = false;
203
						foreach ($value as $key2 => $value2) {
204
							$out->{$key2} = Set::__map($value2, true);
205
						}
206
					} else {
207
						if (!is_numeric($key)) {
208
							$out->{$key} = Set::__map($value, true, $key);
209
							if (is_object($out->{$key}) && !is_numeric($key)) {
210
								if (!isset($out->{$key}->_name_)) {
211
									$out->{$key}->_name_ = $key;
212
								}
213
							}
214
						} else {
215
							$out->{$key} = Set::__map($value, true);
216
						}
217
					}
218
				} else {
219
					$out->{$key} = $value;
220
				}
221
			}
222
		} else {
223
			$out = $array;
224
		}
225
		return $out;
226
	}
227
/**
228
 * Checks to see if all the values in the array are numeric
229
 *
230
 * @param array $array The array to check.  If null, the value of the current Set object
231
 * @return boolean true if values are numeric, false otherwise
232
 * @access public
233
 * @static
234
 */
235
	function numeric($array = null) {
236
		if (empty($array)) {
237
			return null;
238
		}
239
 
240
		if ($array === range(0, count($array) - 1)) {
241
			return true;
242
		}
243
 
244
		$numeric = true;
245
		$keys = array_keys($array);
246
		$count = count($keys);
247
 
248
		for ($i = 0; $i < $count; $i++) {
249
			if (!is_numeric($array[$keys[$i]])) {
250
				$numeric = false;
251
				break;
252
			}
253
		}
254
		return $numeric;
255
	}
256
/**
257
 * Return a value from an array list if the key exists.
258
 *
259
 * If a comma separated $list is passed arrays are numeric with the key of the first being 0
260
 * $list = 'no, yes' would translate to  $list = array(0 => 'no', 1 => 'yes');
261
 *
262
 * If an array is used, keys can be strings example: array('no' => 0, 'yes' => 1);
263
 *
264
 * $list defaults to 0 = no 1 = yes if param is not passed
265
 *
266
 * @param mixed $select Key in $list to return
267
 * @param mixed $list can be an array or a comma-separated list.
268
 * @return string the value of the array key or null if no match
269
 * @access public
270
 * @static
271
 */
272
	function enum($select, $list = null) {
273
		if (empty($list)) {
274
			$list = array('no', 'yes');
275
		}
276
 
277
		$return = null;
278
		$list = Set::normalize($list, false);
279
 
280
		if (array_key_exists($select, $list)) {
281
			$return = $list[$select];
282
		}
283
		return $return;
284
	}
285
/**
286
 * Returns a series of values extracted from an array, formatted in a format string.
287
 *
288
 * @param array		$data Source array from which to extract the data
289
 * @param string	$format Format string into which values will be inserted, see sprintf()
290
 * @param array		$keys An array containing one or more Set::extract()-style key paths
291
 * @return array	An array of strings extracted from $keys and formatted with $format
292
 * @access public
293
 * @static
294
 */
295
	function format($data, $format, $keys) {
296
 
297
		$extracted = array();
298
		$count = count($keys);
299
 
300
		if (!$count) {
301
			return;
302
		}
303
 
304
		for ($i = 0; $i < $count; $i++) {
305
			$extracted[] = Set::extract($data, $keys[$i]);
306
		}
307
		$out = array();
308
		$data = $extracted;
309
		$count = count($data[0]);
310
 
311
		if (preg_match_all('/\{([0-9]+)\}/msi', $format, $keys2) && isset($keys2[1])) {
312
			$keys = $keys2[1];
313
			$format = preg_split('/\{([0-9]+)\}/msi', $format);
314
			$count2 = count($format);
315
 
316
			for ($j = 0; $j < $count; $j++) {
317
				$formatted = '';
318
				for ($i = 0; $i <= $count2; $i++) {
319
					if (isset($format[$i])) {
320
						$formatted .= $format[$i];
321
					}
322
					if (isset($keys[$i]) && isset($data[$keys[$i]][$j])) {
323
						$formatted .= $data[$keys[$i]][$j];
324
					}
325
				}
326
				$out[] = $formatted;
327
			}
328
		} else {
329
			$count2 = count($data);
330
			for ($j = 0; $j < $count; $j++) {
331
				$args = array();
332
				for ($i = 0; $i < $count2; $i++) {
333
					if (isset($data[$i][$j])) {
334
						$args[] = $data[$i][$j];
335
					}
336
				}
337
				$out[] = vsprintf($format, $args);
338
			}
339
		}
340
		return $out;
341
	}
342
/**
343
 * Implements partial support for XPath 2.0. If $path is an array or $data is empty it the call is delegated to Set::classicExtract.
344
 *
345
 * Currently implemented selectors:
346
 * - /User/id (similar to the classic {n}.User.id)
347
 * - /User[2]/name (selects the name of the second User)
348
 * - /User[id>2] (selects all Users with an id > 2)
349
 * - /User[id>2][<5] (selects all Users with an id > 2 but < 5)
350
 * - /Post/Comment[author_name=john]/../name (Selects the name of all Posts that have at least one Comment written by john)
351
 * - /Posts[name] (Selects all Posts that have a 'name' key)
352
 * - /Comment/.[1] (Selects the contents of the first comment)
353
 * - /Comment/.[:last] (Selects the last comment)
354
 * - /Comment/.[:first] (Selects the first comment)
355
 * - /Comment[text=/cakephp/i] (Selects the all comments that have a text matching the regex /cakephp/i)
356
 * - /Comment/@* (Selects the all key names of all comments)
357
 *
358
 * Other limitations:
359
 * - Only absolute paths starting with a single '/' are supported right now
360
 *
361
 * Warning: Even so it has plenty of unit tests the XPath support has not gone through a lot of real-world testing. Please report
362
 * Bugs as you find them. Suggestions for additional features to imlement are also very welcome!
363
 *
364
 * @param string $path An absolute XPath 2.0 path
365
 * @param string $data An array of data to extract from
366
 * @param string $options Currently only supports 'flatten' which can be disabled for higher XPath-ness
367
 * @return array An array of matched items
368
 * @access public
369
 * @static
370
 */
371
	function extract($path, $data = null, $options = array()) {
372
		if (empty($data) && is_string($path) && $path{0} === '/') {
373
			return array();
374
		}
375
		if (is_string($data) && $data{0} === '/') {
376
			$tmp = $path;
377
			$path = $data;
378
			$data = $tmp;
379
		}
380
		if (is_array($path) || empty($data) || is_object($path) || empty($path)) {
381
			return Set::classicExtract($path, $data);
382
		}
383
		if ($path === '/') {
384
			return $data;
385
		}
386
		$contexts = $data;
387
		$options = array_merge(array('flatten' => true), $options);
388
		if (!isset($contexts[0])) {
389
			$contexts = array($data);
390
		}
391
		$tokens = array_slice(preg_split('/(?<!=)\/(?![a-z]*\])/', $path), 1);
392
 
393
		do {
394
			$token = array_shift($tokens);
395
			$conditions = false;
396
			if (preg_match_all('/\[([^\]]+)\]/', $token, $m)) {
397
				$conditions = $m[1];
398
				$token = substr($token, 0, strpos($token, '['));
399
			}
400
			$matches = array();
401
			foreach ($contexts as $key => $context) {
402
				if (!isset($context['trace'])) {
403
					$context = array('trace' => array(null), 'item' => $context, 'key' => $key);
404
				}
405
				if ($token === '..') {
406
					if (count($context['trace']) == 1) {
407
						$context['trace'][] = $context['key'];
408
					}
409
					$parent = join('/', $context['trace']) . '/.';
410
					$context['item'] = Set::extract($parent, $data);
411
					$context['key'] = array_pop($context['trace']);
412
					if (isset($context['trace'][1]) && $context['trace'][1] > 0) {
413
						$context['item'] = $context['item'][0];
414
					} else {
415
						$context['item'] = $context['item'][$key];
416
					}
417
					$matches[] = $context;
418
					continue;
419
				}
420
				$match = false;
421
				if ($token === '@*' && is_array($context['item'])) {
422
					$matches[] = array(
423
						'trace' => array_merge($context['trace'], (array)$key),
424
						'key' => $key,
425
						'item' => array_keys($context['item']),
426
					);
427
				} elseif (is_array($context['item']) && array_key_exists($token, $context['item'])) {
428
					$items = $context['item'][$token];
429
					if (!is_array($items)) {
430
						$items = array($items);
431
					} elseif (!isset($items[0])) {
432
						$current = current($items);
433
						if ((is_array($current) && count($items) <= 1) || !is_array($current)) {
434
							$items = array($items);
435
						}
436
					}
437
 
438
					foreach ($items as $key => $item) {
439
						$ctext = array($context['key']);
440
						if (!is_numeric($key)) {
441
							$ctext[] = $token;
442
							$token = array_shift($tokens);
443
							if (isset($items[$token])) {
444
								$ctext[] = $token;
445
								$item = $items[$token];
446
								$matches[] = array(
447
									'trace' => array_merge($context['trace'], $ctext),
448
									'key' => $key,
449
									'item' => $item,
450
								);
451
								break;
452
							}
453
						} else {
454
							$key = $token;
455
						}
456
 
457
						$matches[] = array(
458
							'trace' => array_merge($context['trace'], $ctext),
459
							'key' => $key,
460
							'item' => $item,
461
						);
462
					}
463
				} elseif (($key === $token || (ctype_digit($token) && $key == $token) || $token === '.')) {
464
					$context['trace'][] = $key;
465
					$matches[] = array(
466
						'trace' => $context['trace'],
467
						'key' => $key,
468
						'item' => $context['item'],
469
					);
470
				}
471
			}
472
			if ($conditions) {
473
				foreach ($conditions as $condition) {
474
					$filtered = array();
475
					$length = count($matches);
476
					foreach ($matches as $i => $match) {
477
						if (Set::matches(array($condition), $match['item'], $i + 1, $length)) {
478
							$filtered[] = $match;
479
						}
480
					}
481
					$matches = $filtered;
482
				}
483
			}
484
			$contexts = $matches;
485
 
486
			if (empty($tokens)) {
487
				break;
488
			}
489
		} while(1);
490
 
491
		$r = array();
492
 
493
		foreach ($matches as $match) {
494
			if ((!$options['flatten'] || is_array($match['item'])) && !is_int($match['key'])) {
495
				$r[] = array($match['key'] => $match['item']);
496
			} else {
497
				$r[] = $match['item'];
498
			}
499
		}
500
		return $r;
501
	}
502
/**
503
 * This function can be used to see if a single item or a given xpath match certain conditions.
504
 *
505
 * @param mixed $conditions An array of condition strings or an XPath expression
506
 * @param array $data  An array of data to execute the match on
507
 * @param integer $i Optional: The 'nth'-number of the item being matched.
508
 * @return boolean
509
 * @access public
510
 * @static
511
 */
512
	function matches($conditions, $data = array(), $i = null, $length = null) {
513
		if (empty($conditions)) {
514
			return true;
515
		}
516
		if (is_string($conditions)) {
517
			return !!Set::extract($conditions, $data);
518
		}
519
		foreach ($conditions as $condition) {
520
			if ($condition === ':last') {
521
				if ($i != $length) {
522
					return false;
523
				}
524
				continue;
525
			} elseif ($condition === ':first') {
526
				if ($i != 1) {
527
					return false;
528
				}
529
				continue;
530
			}
531
			if (!preg_match('/(.+?)([><!]?[=]|[><])(.*)/', $condition, $match)) {
532
				if (ctype_digit($condition)) {
533
					if ($i != $condition) {
534
						return false;
535
					}
536
				} elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) {
537
					return in_array($i, $matches[0]);
538
				} elseif (!array_key_exists($condition, $data)) {
539
					return false;
540
				}
541
				continue;
542
			}
543
			list(,$key,$op,$expected) = $match;
544
			if (!isset($data[$key])) {
545
				return false;
546
			}
547
 
548
			$val = $data[$key];
549
 
550
			if ($op === '=' && $expected && $expected{0} === '/') {
551
				return preg_match($expected, $val);
552
			}
553
			if ($op === '=' && $val != $expected) {
554
				return false;
555
			}
556
			if ($op === '!=' && $val == $expected) {
557
				return false;
558
			}
559
			if ($op === '>' && $val <= $expected) {
560
				return false;
561
			}
562
			if ($op === '<' && $val >= $expected) {
563
				return false;
564
			}
565
			if ($op === '<=' && $val > $expected) {
566
				return false;
567
			}
568
			if ($op === '>=' && $val < $expected) {
569
				return false;
570
			}
571
		}
572
		return true;
573
	}
574
/**
575
 * Gets a value from an array or object that is contained in a given path using an array path syntax, i.e.:
576
 * "{n}.Person.{[a-z]+}" - Where "{n}" represents a numeric key, "Person" represents a string literal,
577
 * and "{[a-z]+}" (i.e. any string literal enclosed in brackets besides {n} and {s}) is interpreted as
578
 * a regular expression.
579
 *
580
 * @param array $data Array from where to extract
581
 * @param mixed $path As an array, or as a dot-separated string.
582
 * @return array Extracted data
583
 * @access public
584
 * @static
585
 */
586
	function classicExtract($data, $path = null) {
587
		if (empty($path)) {
588
			return $data;
589
		}
590
		if (is_object($data)) {
591
			$data = get_object_vars($data);
592
		}
593
		if (!is_array($data)) {
594
			return $data;
595
		}
596
 
597
		if (!is_array($path)) {
598
			if (!class_exists('String')) {
599
				App::import('Core', 'String');
600
			}
601
			$path = String::tokenize($path, '.', '{', '}');
602
		}
603
		$tmp = array();
604
 
605
		if (!is_array($path) || empty($path)) {
606
			return null;
607
		}
608
 
609
		foreach ($path as $i => $key) {
610
			if (is_numeric($key) && intval($key) > 0 || $key === '0') {
611
				if (isset($data[intval($key)])) {
612
					$data = $data[intval($key)];
613
				} else {
614
					return null;
615
				}
616
			} elseif ($key === '{n}') {
617
				foreach ($data as $j => $val) {
618
					if (is_int($j)) {
619
						$tmpPath = array_slice($path, $i + 1);
620
						if (empty($tmpPath)) {
621
							$tmp[] = $val;
622
						} else {
623
							$tmp[] = Set::classicExtract($val, $tmpPath);
624
						}
625
					}
626
				}
627
				return $tmp;
628
			} elseif ($key === '{s}') {
629
				foreach ($data as $j => $val) {
630
					if (is_string($j)) {
631
						$tmpPath = array_slice($path, $i + 1);
632
						if (empty($tmpPath)) {
633
							$tmp[] = $val;
634
						} else {
635
							$tmp[] = Set::classicExtract($val, $tmpPath);
636
						}
637
					}
638
				}
639
				return $tmp;
640
			} elseif (false !== strpos($key,'{') && false !== strpos($key,'}')) {
641
				$pattern = substr($key, 1, -1);
642
 
643
				foreach ($data as $j => $val) {
644
					if (preg_match('/^'.$pattern.'/s', $j) !== 0) {
645
						$tmpPath = array_slice($path, $i + 1);
646
						if (empty($tmpPath)) {
647
							$tmp[$j] = $val;
648
						} else {
649
							$tmp[$j] = Set::classicExtract($val, $tmpPath);
650
						}
651
					}
652
				}
653
				return $tmp;
654
			} else {
655
				if (isset($data[$key])) {
656
					$data = $data[$key];
657
				} else {
658
					return null;
659
				}
660
			}
661
		}
662
		return $data;
663
	}
664
/**
665
 * Inserts $data into an array as defined by $path.
666
 *
667
 * @param mixed $list Where to insert into
668
 * @param mixed $path A dot-separated string.
669
 * @param array $data Data to insert
670
 * @return array
671
 * @access public
672
 * @static
673
 */
674
	function insert($list, $path, $data = null) {
675
		if (!is_array($path)) {
676
			$path = explode('.', $path);
677
		}
678
		$_list =& $list;
679
 
680
		foreach ($path as $i => $key) {
681
			if (is_numeric($key) && intval($key) > 0 || $key === '0') {
682
				$key = intval($key);
683
			}
684
			if ($i === count($path) - 1) {
685
				$_list[$key] = $data;
686
			} else {
687
				if (!isset($_list[$key])) {
688
					$_list[$key] = array();
689
				}
690
				$_list =& $_list[$key];
691
			}
692
		}
693
		return $list;
694
	}
695
/**
696
 * Removes an element from a Set or array as defined by $path.
697
 *
698
 * @param mixed $list From where to remove
699
 * @param mixed $path A dot-separated string.
700
 * @return array Array with $path removed from its value
701
 * @access public
702
 * @static
703
 */
704
	function remove($list, $path = null) {
705
		if (empty($path)) {
706
			return $list;
707
		}
708
		if (!is_array($path)) {
709
			$path = explode('.', $path);
710
		}
711
		$_list =& $list;
712
 
713
		foreach ($path as $i => $key) {
714
			if (is_numeric($key) && intval($key) > 0 || $key === '0') {
715
				$key = intval($key);
716
			}
717
			if ($i === count($path) - 1) {
718
				unset($_list[$key]);
719
			} else {
720
				if (!isset($_list[$key])) {
721
					return $list;
722
				}
723
				$_list =& $_list[$key];
724
			}
725
		}
726
		return $list;
727
	}
728
/**
729
 * Checks if a particular path is set in an array
730
 *
731
 * @param mixed $data Data to check on
732
 * @param mixed $path A dot-separated string.
733
 * @return boolean true if path is found, false otherwise
734
 * @access public
735
 * @static
736
 */
737
	function check($data, $path = null) {
738
		if (empty($path)) {
739
			return $data;
740
		}
741
		if (!is_array($path)) {
742
			$path = explode('.', $path);
743
		}
744
 
745
		foreach ($path as $i => $key) {
746
			if (is_numeric($key) && intval($key) > 0 || $key === '0') {
747
				$key = intval($key);
748
			}
749
			if ($i === count($path) - 1) {
750
				return (is_array($data) && array_key_exists($key, $data));
751
			}
752
 
753
			if (!is_array($data) || !array_key_exists($key, $data)) {
754
				return false;
755
			}
756
			$data =& $data[$key];
757
		}
758
		return true;
759
	}
760
/**
761
 * Computes the difference between a Set and an array, two Sets, or two arrays
762
 *
763
 * @param mixed $val1 First value
764
 * @param mixed $val2 Second value
765
 * @return array Computed difference
766
 * @access public
767
 * @static
768
 */
769
	function diff($val1, $val2 = null) {
770
		if (empty($val1)) {
771
			return (array)$val2;
772
		}
773
		if (empty($val2)) {
774
			return (array)$val1;
775
		}
776
		$out = array();
777
 
778
		foreach ($val1 as $key => $val) {
779
			$exists = array_key_exists($key, $val2);
780
 
781
			if ($exists && $val2[$key] != $val) {
782
				$out[$key] = $val;
783
			} elseif (!$exists) {
784
				$out[$key] = $val;
785
			}
786
			unset($val2[$key]);
787
		}
788
 
789
		foreach ($val2 as $key => $val) {
790
			if (!array_key_exists($key, $out)) {
791
				$out[$key] = $val;
792
			}
793
		}
794
		return $out;
795
	}
796
/**
797
 * Determines if two Sets or arrays are equal
798
 *
799
 * @param array $val1 First value
800
 * @param array $val2 Second value
801
 * @return boolean true if they are equal, false otherwise
802
 * @access public
803
 * @static
804
 */
805
	function isEqual($val1, $val2 = null) {
806
		return ($val1 == $val2);
807
	}
808
/**
809
 * Determines if one Set or array contains the exact keys and values of another.
810
 *
811
 * @param array $val1 First value
812
 * @param array $val2 Second value
813
 * @return boolean true if $val1 contains $val2, false otherwise
814
 * @access public
815
 * @static
816
 */
817
	function contains($val1, $val2 = null) {
818
		if (empty($val1) || empty($val2)) {
819
			return false;
820
		}
821
 
822
		foreach ($val2 as $key => $val) {
823
			if (is_numeric($key)) {
824
				Set::contains($val, $val1);
825
			} else {
826
				if (!isset($val1[$key]) || $val1[$key] != $val) {
827
					return false;
828
				}
829
			}
830
		}
831
		return true;
832
	}
833
/**
834
 * Counts the dimensions of an array. If $all is set to false (which is the default) it will
835
 * only consider the dimension of the first element in the array.
836
 *
837
 * @param array $array Array to count dimensions on
838
 * @param boolean $all Set to true to count the dimension considering all elements in array
839
 * @param integer $count Start the dimension count at this number
840
 * @return integer The number of dimensions in $array
841
 * @access public
842
 * @static
843
 */
844
	function countDim($array = null, $all = false, $count = 0) {
845
		if ($all) {
846
			$depth = array($count);
847
			if (is_array($array) && reset($array) !== false) {
848
				foreach ($array as $value) {
849
					$depth[] = Set::countDim($value, true, $count + 1);
850
				}
851
			}
852
			$return = max($depth);
853
		} else {
854
			if (is_array(reset($array))) {
855
				$return = Set::countDim(reset($array)) + 1;
856
			} else {
857
				$return = 1;
858
			}
859
		}
860
		return $return;
861
	}
862
/**
863
 * Normalizes a string or array list.
864
 *
865
 * @param mixed $list List to normalize
866
 * @param boolean $assoc If true, $list will be converted to an associative array
867
 * @param string $sep If $list is a string, it will be split into an array with $sep
868
 * @param boolean $trim If true, separated strings will be trimmed
869
 * @return array
870
 * @access public
871
 * @static
872
 */
873
	function normalize($list, $assoc = true, $sep = ',', $trim = true) {
874
		if (is_string($list)) {
875
			$list = explode($sep, $list);
876
			if ($trim) {
877
				foreach ($list as $key => $value) {
878
					$list[$key] = trim($value);
879
				}
880
			}
881
			if ($assoc) {
882
				return Set::normalize($list);
883
			}
884
		} elseif (is_array($list)) {
885
			$keys = array_keys($list);
886
			$count = count($keys);
887
			$numeric = true;
888
 
889
			if (!$assoc) {
890
				for ($i = 0; $i < $count; $i++) {
891
					if (!is_int($keys[$i])) {
892
						$numeric = false;
893
						break;
894
					}
895
				}
896
			}
897
			if (!$numeric || $assoc) {
898
				$newList = array();
899
				for ($i = 0; $i < $count; $i++) {
900
					if (is_int($keys[$i])) {
901
						$newList[$list[$keys[$i]]] = null;
902
					} else {
903
						$newList[$keys[$i]] = $list[$keys[$i]];
904
					}
905
				}
906
				$list = $newList;
907
			}
908
		}
909
		return $list;
910
	}
911
/**
912
 * Creates an associative array using a $path1 as the path to build its keys, and optionally
913
 * $path2 as path to get the values. If $path2 is not specified, all values will be initialized
914
 * to null (useful for Set::merge). You can optionally group the values by what is obtained when
915
 * following the path specified in $groupPath.
916
 *
917
 * @param array $data Array from where to extract keys and values
918
 * @param mixed $path1 As an array, or as a dot-separated string.
919
 * @param mixed $path2 As an array, or as a dot-separated string.
920
 * @param string $groupPath As an array, or as a dot-separated string.
921
 * @return array Combined array
922
 * @access public
923
 * @static
924
 */
925
	function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
926
		if (empty($data)) {
927
			return array();
928
		}
929
 
930
		if (is_object($data)) {
931
			$data = get_object_vars($data);
932
		}
933
 
934
		if (is_array($path1)) {
935
			$format = array_shift($path1);
936
			$keys = Set::format($data, $format, $path1);
937
		} else {
938
			$keys = Set::extract($data, $path1);
939
		}
940
 
941
		if (!empty($path2) && is_array($path2)) {
942
			$format = array_shift($path2);
943
			$vals = Set::format($data, $format, $path2);
944
 
945
		} elseif (!empty($path2)) {
946
			$vals = Set::extract($data, $path2);
947
 
948
		} else {
949
			$count = count($keys);
950
			for ($i = 0; $i < $count; $i++) {
951
				$vals[$i] = null;
952
			}
953
		}
954
 
955
		if ($groupPath != null) {
956
			$group = Set::extract($data, $groupPath);
957
			if (!empty($group)) {
958
				$c = count($keys);
959
				for ($i = 0; $i < $c; $i++) {
960
					if (!isset($group[$i])) {
961
						$group[$i] = 0;
962
					}
963
					if (!isset($out[$group[$i]])) {
964
						$out[$group[$i]] = array();
965
					}
966
					$out[$group[$i]][$keys[$i]] = $vals[$i];
967
				}
968
				return $out;
969
			}
970
		}
971
 
972
		return array_combine($keys, $vals);
973
	}
974
/**
975
 * Converts an object into an array. If $object is no object, reverse
976
 * will return the same value.
977
 *
978
 * @param object $object Object to reverse
979
 * @return array
980
 * @static
981
 */
982
	function reverse($object) {
983
		$out = array();
984
		if (is_a($object, 'XmlNode')) {
985
			$out = $object->toArray();
986
			return $out;
987
		} else if (is_object($object)) {
988
			$keys = get_object_vars($object);
989
			if (isset($keys['_name_'])) {
990
				$identity = $keys['_name_'];
991
				unset($keys['_name_']);
992
			}
993
			$new = array();
994
			foreach ($keys as $key => $value) {
995
				if (is_array($value)) {
996
					$new[$key] = (array)Set::reverse($value);
997
				} else {
998
					if (isset($value->_name_)) {
999
						$new = array_merge($new, Set::reverse($value));
1000
					} else {
1001
						$new[$key] = Set::reverse($value);
1002
					}
1003
				}
1004
			}
1005
			if (isset($identity)) {
1006
				$out[$identity] = $new;
1007
			} else {
1008
				$out = $new;
1009
			}
1010
		} elseif (is_array($object)) {
1011
			foreach ($object as $key => $value) {
1012
				$out[$key] = Set::reverse($value);
1013
			}
1014
		} else {
1015
			$out = $object;
1016
		}
1017
		return $out;
1018
	}
1019
/**
1020
 * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
1021
 * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
1022
 * array('0.Foo.Bar' => 'Far').
1023
 *
1024
 * @param array $data Array to flatten
1025
 * @param string $separator String used to separate array key elements in a path, defaults to '.'
1026
 * @return array
1027
 * @access public
1028
 * @static
1029
 */
1030
	function flatten($data, $separator = '.') {
1031
		$result = array();
1032
		$path = null;
1033
 
1034
		if (is_array($separator)) {
1035
			extract($separator, EXTR_OVERWRITE);
1036
		}
1037
 
1038
		if (!is_null($path)) {
1039
			$path .= $separator;
1040
		}
1041
 
1042
		foreach ($data as $key => $val) {
1043
			if (is_array($val)) {
1044
				$result += (array)Set::flatten($val, array(
1045
					'separator' => $separator,
1046
					'path' => $path . $key
1047
				));
1048
			} else {
1049
				$result[$path . $key] = $val;
1050
			}
1051
		}
1052
		return $result;
1053
	}
1054
/**
1055
 * Flattens an array for sorting
1056
 *
1057
 * @param array $results
1058
 * @param string $key
1059
 * @return array
1060
 * @access private
1061
 */
1062
	function __flatten($results, $key = null) {
1063
		$stack = array();
1064
		foreach ($results as $k => $r) {
1065
			$id = $k;
1066
			if (!is_null($key)) {
1067
				$id = $key;
1068
			}
1069
			if (is_array($r)) {
1070
				$stack = array_merge($stack, Set::__flatten($r, $id));
1071
			} else {
1072
				$stack[] = array('id' => $id, 'value' => $r);
1073
			}
1074
		}
1075
		return $stack;
1076
	}
1077
/**
1078
 * Sorts an array by any value, determined by a Set-compatible path
1079
 *
1080
 * @param array $data
1081
 * @param string $path A Set-compatible path to the array value
1082
 * @param string $dir asc/desc
1083
 * @return array
1084
 * @static
1085
 */
1086
	function sort($data, $path, $dir) {
1087
		$result = Set::__flatten(Set::extract($data, $path));
1088
		list($keys, $values) = array(Set::extract($result, '{n}.id'), Set::extract($result, '{n}.value'));
1089
 
1090
		$dir = strtolower($dir);
1091
		if ($dir === 'asc') {
1092
			$dir = SORT_ASC;
1093
		} elseif ($dir === 'desc') {
1094
			$dir = SORT_DESC;
1095
		}
1096
		array_multisort($values, $dir, $keys, $dir);
1097
		$sorted = array();
1098
 
1099
		$keys = array_unique($keys);
1100
 
1101
		foreach ($keys as $k) {
1102
			$sorted[] = $data[$k];
1103
		}
1104
		return $sorted;
1105
	}
1106
/**
1107
 * Deprecated, Set class should be called statically
1108
 *
1109
 */
1110
	function &get() {
1111
		trigger_error('get() is deprecated. Set class should be called statically', E_USER_WARNING);
1112
	}
1113
}
1114
?>