PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/Cake/Utility/Set.php

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