Set.php 30 KB

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