PageRenderTime 57ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/cake/libs/set.php

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