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