Set.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\util;
  9. /**
  10. * Used for complex manipulation, comparison, and access of array data. Some methods allow for
  11. * XPath-like data access, as follows:
  12. *
  13. * - `'/User/id'`: Similar to the classic {n}.User.id.
  14. * - `'/User[2]/name'`: Selects the name of the second User.
  15. * - `'/User[id>2]'`: Selects all Users with an id > 2.
  16. * - `'/User[id>2][<5]'`: Selects all Users with an id > 2 but < 5.
  17. * - `'/Post/Comment[author_name=John]/../name'`: Selects the name of
  18. * all posts that have at least one comment written by John.
  19. * - `'/Posts[name]'`: Selects all Posts that have a `'name'` key.
  20. * - `'/Comment/.[1]'`: Selects the contents of the first comment.
  21. * - `'/Comment/.[:last]'`: Selects the last comment.
  22. * - `'/Comment/.[:first]'`: Selects the first comment.
  23. * - `'/Comment[text=/lithium/i]`': Selects the all comments that have
  24. * a text matching the regex `/lithium/i`.
  25. * - `'/Comment/@*'`: Selects all key names of all comments.
  26. */
  27. class Set {
  28. /**
  29. * Add the keys/values in `$array2` that are not found in `$array` onto the end of `$array`.
  30. *
  31. * @param mixed $array Original array.
  32. * @param mixed $array2 Second array to add onto the original.
  33. * @return array An array containing all the keys of the second array not already present in the
  34. * first.
  35. */
  36. public static function append(array $array, array $array2) {
  37. $arrays = func_get_args();
  38. $array = array_shift($arrays);
  39. foreach ($arrays as $array2) {
  40. if (!$array && $array2) {
  41. $array = $array2;
  42. continue;
  43. }
  44. foreach ($array2 as $key => $value) {
  45. if (!array_key_exists($key, $array)) {
  46. $array[$key] = $value;
  47. } elseif (is_array($value)) {
  48. $array[$key] = static::append($array[$key], $array2[$key]);
  49. }
  50. }
  51. }
  52. return $array;
  53. }
  54. /**
  55. * Checks if a particular path is set in an array. Tests by key name, or dot-delimited key
  56. * name, i.e.:
  57. *
  58. * {{{ embed:lithium\tests\cases\util\SetTest::testCheck(1-4) }}}
  59. *
  60. * @param mixed $data Data to check on.
  61. * @param mixed $path A dot-delimited string.
  62. * @return boolean `true` if path is found, `false` otherwise.
  63. */
  64. public static function check($data, $path = null) {
  65. if (!$path) {
  66. return $data;
  67. }
  68. $path = is_array($path) ? $path : explode('.', $path);
  69. foreach ($path as $i => $key) {
  70. if (is_numeric($key) && intval($key) > 0 || $key === '0') {
  71. $key = intval($key);
  72. }
  73. if ($i === count($path) - 1) {
  74. return (is_array($data) && isset($data[$key]));
  75. } else {
  76. if (!is_array($data) || !isset($data[$key])) {
  77. return false;
  78. }
  79. $data =& $data[$key];
  80. }
  81. }
  82. }
  83. /**
  84. * Creates an associative array using a `$path1` as the path to build its keys, and optionally
  85. * `$path2` as path to get the values. If `$path2` is not specified, all values will be
  86. * initialized to `null` (useful for `Set::merge()`). You can optionally group the values by
  87. * what is obtained when following the path specified in `$groupPath`.
  88. *
  89. * @param array $data Array from where to extract keys and values.
  90. * @param mixed $path1 As an array, or as a dot-delimited string.
  91. * @param mixed $path2 As an array, or as a dot-delimited string.
  92. * @param string $groupPath As an array, or as a dot-delimited string.
  93. * @return array Combined array.
  94. */
  95. public static function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
  96. if (!$data) {
  97. return array();
  98. }
  99. if (is_object($data)) {
  100. $data = get_object_vars($data);
  101. }
  102. if (is_array($path1)) {
  103. $format = array_shift($path1);
  104. $keys = static::format($data, $format, $path1);
  105. } else {
  106. $keys = static::extract($data, $path1);
  107. }
  108. $vals = array();
  109. if (!empty($path2) && is_array($path2)) {
  110. $format = array_shift($path2);
  111. $vals = static::format($data, $format, $path2);
  112. } elseif (!empty($path2)) {
  113. $vals = static::extract($data, $path2);
  114. }
  115. $valCount = count($vals);
  116. $count = count($keys);
  117. for ($i = $valCount; $i < $count; $i++) {
  118. $vals[$i] = null;
  119. }
  120. if ($groupPath) {
  121. $group = static::extract($data, $groupPath);
  122. if (!empty($group)) {
  123. $c = count($keys);
  124. for ($i = 0; $i < $c; $i++) {
  125. if (!isset($group[$i])) {
  126. $group[$i] = 0;
  127. }
  128. if (!isset($out[$group[$i]])) {
  129. $out[$group[$i]] = array();
  130. }
  131. $out[$group[$i]][$keys[$i]] = $vals[$i];
  132. }
  133. return $out;
  134. }
  135. }
  136. return array_combine($keys, $vals);
  137. }
  138. /**
  139. * Determines if the array elements in `$array2` are wholly contained within `$array1`. Works
  140. * recursively.
  141. *
  142. * @param array $array1 First value.
  143. * @param array $array2 Second value.
  144. * @return boolean Returns `true` if `$array1` wholly contains the keys and values of `$array2`,
  145. * otherwise, returns `false`. Returns `false` if either array is empty.
  146. */
  147. public static function contains(array $array1, array $array2) {
  148. if (!$array1 || !$array2) {
  149. return false;
  150. }
  151. foreach ($array2 as $key => $val) {
  152. if (!isset($array1[$key]) || $array1[$key] !== $val) {
  153. return false;
  154. }
  155. if (is_array($val) && !static::contains($array1[$key], $val)) {
  156. return false;
  157. }
  158. }
  159. return true;
  160. }
  161. /**
  162. * Counts the dimensions of an array. If `$all` is set to `false` (which is the default) it will
  163. * only consider the dimension of the first element in the array.
  164. *
  165. * @param array $data Array to count dimensions on.
  166. * @param array $options
  167. * @return integer The number of dimensions in `$array`.
  168. */
  169. public static function depth($data, array $options = array()) {
  170. $defaults = array('all' => false, 'count' => 0);
  171. $options += $defaults;
  172. if (!$data) {
  173. return 0;
  174. }
  175. if (!$options['all']) {
  176. return (is_array(reset($data))) ? static::depth(reset($data)) + 1 : 1;
  177. }
  178. $depth = array($options['count']);
  179. if (is_array($data) && reset($data) !== false) {
  180. foreach ($data as $value) {
  181. $depth[] = static::depth($value, array(
  182. 'all' => $options['all'],
  183. 'count' => $options['count'] + 1
  184. ));
  185. }
  186. }
  187. return max($depth);
  188. }
  189. /**
  190. * Computes the difference between two arrays.
  191. *
  192. * @param array $val1 First value.
  193. * @param array $val2 Second value.
  194. * @return array Computed difference.
  195. */
  196. public static function diff(array $val1, array $val2) {
  197. if (!$val1 || !$val2) {
  198. return $val2 ?: $val1;
  199. }
  200. $out = array();
  201. foreach ($val1 as $key => $val) {
  202. $exists = isset($val2[$key]);
  203. if (($exists && $val2[$key] !== $val) || !$exists) {
  204. $out[$key] = $val;
  205. }
  206. unset($val2[$key]);
  207. }
  208. foreach ($val2 as $key => $val) {
  209. if (!isset($out[$key])) {
  210. $out[$key] = $val;
  211. }
  212. }
  213. return $out;
  214. }
  215. /**
  216. * Implements partial support for XPath 2.0.
  217. *
  218. * @param array $data An array of data to extract from.
  219. * @param string $path An absolute XPath 2.0 path. Only absolute paths starting with a
  220. * single slash are supported right now. Implemented selectors:
  221. * - `'/User/id'`: Similar to the classic {n}.User.id.
  222. * - `'/User[2]/name'`: Selects the name of the second User.
  223. * - `'/User[id>2]'`: Selects all Users with an id > 2.
  224. * - `'/User[id>2][<5]'`: Selects all Users with an id > 2 but < 5.
  225. * - `'/Post/Comment[author_name=John]/../name'`: Selects the name of
  226. * all posts that have at least one comment written by John.
  227. * - `'/Posts[name]'`: Selects all Posts that have a `'name'` key.
  228. * - `'/Comment/.[1]'`: Selects the contents of the first comment.
  229. * - `'/Comment/.[:last]'`: Selects the last comment.
  230. * - `'/Comment/.[:first]'`: Selects the first comment.
  231. * - `'/Comment[text=/lithium/i]`': Selects the all comments that have
  232. * a text matching the regex `/lithium/i`.
  233. * - `'/Comment/@*'`: Selects all key names of all comments.
  234. * @param array $options Currently only supports `'flatten'` which can be
  235. * disabled for higher XPath-ness.
  236. * @return array An array of matched items.
  237. */
  238. public static function extract(array $data, $path = null, array $options = array()) {
  239. if (!$data) {
  240. return array();
  241. }
  242. if (is_string($data)) {
  243. $tmp = $path;
  244. $path = $data;
  245. $data = $tmp;
  246. unset($tmp);
  247. }
  248. if ($path === '/') {
  249. return array_filter($data, function($data) {
  250. return ($data === 0 || $data === '0' || !empty($data));
  251. });
  252. }
  253. $contexts = $data;
  254. $defaults = array('flatten' => true);
  255. $options += $defaults;
  256. if (!isset($contexts[0])) {
  257. $contexts = array($data);
  258. }
  259. $tokens = array_slice(preg_split('/(?<!=)\/(?![a-z-]*\])/', $path), 1);
  260. do {
  261. $token = array_shift($tokens);
  262. $conditions = false;
  263. if (preg_match_all('/\[([^=]+=\/[^\/]+\/|[^\]]+)\]/', $token, $m)) {
  264. $conditions = $m[1];
  265. $token = substr($token, 0, strpos($token, '['));
  266. }
  267. $matches = array();
  268. foreach ($contexts as $key => $context) {
  269. if (!isset($context['trace'])) {
  270. $context = array('trace' => array(null), 'item' => $context, 'key' => $key);
  271. }
  272. if ($token === '..') {
  273. if (count($context['trace']) === 1) {
  274. $context['trace'][] = $context['key'];
  275. }
  276. array_pop($context['trace']);
  277. $parent = join('/', $context['trace']);
  278. $context['item'] = static::extract($data, $parent);
  279. array_pop($context['trace']);
  280. $context['item'] = array_shift($context['item']);
  281. $matches[] = $context;
  282. continue;
  283. }
  284. $match = false;
  285. if ($token === '@*' && is_array($context['item'])) {
  286. $matches[] = array(
  287. 'trace' => array_merge($context['trace'], (array) $key),
  288. 'key' => $key,
  289. 'item' => array_keys($context['item'])
  290. );
  291. } elseif (is_array($context['item']) && isset($context['item'][$token])) {
  292. $items = $context['item'][$token];
  293. if (!is_array($items)) {
  294. $items = array($items);
  295. } elseif (!isset($items[0])) {
  296. $current = current($items);
  297. if ((is_array($current) && count($items) <= 1) || !is_array($current)) {
  298. $items = array($items);
  299. }
  300. }
  301. foreach ($items as $key => $item) {
  302. $ctext = array($context['key']);
  303. if (!is_numeric($key)) {
  304. $ctext[] = $token;
  305. $token = array_shift($tokens);
  306. if (isset($items[$token])) {
  307. $ctext[] = $token;
  308. $item = $items[$token];
  309. $matches[] = array(
  310. 'trace' => array_merge($context['trace'], $ctext),
  311. 'key' => $key,
  312. 'item' => $item
  313. );
  314. break;
  315. } else {
  316. array_unshift($tokens, $token);
  317. }
  318. } else {
  319. $ctext[] = $token;
  320. }
  321. $matches[] = array(
  322. 'trace' => array_merge($context['trace'], $ctext),
  323. 'key' => $key,
  324. 'item' => $item
  325. );
  326. }
  327. } elseif (
  328. $key === $token || (ctype_digit($token) && $key == $token) || $token === '.'
  329. ) {
  330. $context['trace'][] = $key;
  331. $matches[] = array(
  332. 'trace' => $context['trace'],
  333. 'key' => $key,
  334. 'item' => $context['item']
  335. );
  336. }
  337. }
  338. if ($conditions) {
  339. foreach ($conditions as $condition) {
  340. $filtered = array();
  341. $length = count($matches);
  342. foreach ($matches as $i => $match) {
  343. if (static::matches($match['item'], array($condition), $i + 1, $length)) {
  344. $filtered[] = $match;
  345. }
  346. }
  347. $matches = $filtered;
  348. }
  349. }
  350. $contexts = $matches;
  351. if (empty($tokens)) {
  352. break;
  353. }
  354. } while (1);
  355. $r = array();
  356. foreach ($matches as $match) {
  357. $key = array_pop($match['trace']);
  358. $condition = (!is_int($key) && $key !== null);
  359. if ((!$options['flatten'] || is_array($match['item'])) && $condition) {
  360. $r[] = array($key => $match['item']);
  361. } else {
  362. $r[] = $match['item'];
  363. }
  364. }
  365. return $r;
  366. }
  367. /**
  368. * Collapses a multi-dimensional array into a single dimension, using a delimited array path
  369. * for each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
  370. * array('0.Foo.Bar' => 'Far').
  371. *
  372. * @param array $data array to flatten
  373. * @param array $options Available options are:
  374. * - `'separator'`: String to separate array keys in path (defaults to `'.'`).
  375. * - `'path'`: Starting point (defaults to null).
  376. * @return array
  377. */
  378. public static function flatten($data, array $options = array()) {
  379. $defaults = array('separator' => '.', 'path' => null);
  380. $options += $defaults;
  381. $result = array();
  382. if (!is_null($options['path'])) {
  383. $options['path'] .= $options['separator'];
  384. }
  385. foreach ($data as $key => $val) {
  386. if (!is_array($val)) {
  387. $result[$options['path'] . $key] = $val;
  388. continue;
  389. }
  390. $opts = array('separator' => $options['separator'], 'path' => $options['path'] . $key);
  391. $result += (array) static::flatten($val, $opts);
  392. }
  393. return $result;
  394. }
  395. /**
  396. * Accepts a one-dimensional array where the keys are separated by a delimiter.
  397. *
  398. * @param array $data The one-dimensional array to expand.
  399. * @param array $options The options used when expanding the array:
  400. * - `'separator'` _string_: The delimiter to use when separating keys. Defaults
  401. * to `'.'`.
  402. * @return array Returns a multi-dimensional array expanded from a one dimensional dot-separated
  403. * array.
  404. */
  405. public static function expand(array $data, array $options = array()) {
  406. $defaults = array('separator' => '.');
  407. $options += $defaults;
  408. $result = array();
  409. foreach ($data as $key => $val) {
  410. if (strpos($key, $options['separator']) === false) {
  411. if (!isset($result[$key])) {
  412. $result[$key] = $val;
  413. }
  414. continue;
  415. }
  416. list($path, $key) = explode($options['separator'], $key, 2);
  417. $path = is_numeric($path) ? intval($path) : $path;
  418. $result[$path][$key] = $val;
  419. }
  420. foreach ($result as $key => $value) {
  421. if (is_array($value)) {
  422. $result[$key] = static::expand($value, $options);
  423. }
  424. }
  425. return $result;
  426. }
  427. /**
  428. * Returns a series of values extracted from an array, formatted in a format string.
  429. *
  430. * @param array $data Source array from which to extract the data.
  431. * @param string $format Format string into which values will be inserted using `sprintf()`.
  432. * @param array $keys An array containing one or more `Set::extract()`-style key paths.
  433. * @return array An array of strings extracted from `$keys` and formatted with `$format`.
  434. * @link http://php.net/sprintf
  435. */
  436. public static function format($data, $format, $keys) {
  437. $extracted = array();
  438. $count = count($keys);
  439. if (!$count) {
  440. return;
  441. }
  442. for ($i = 0; $i < $count; $i++) {
  443. $extracted[] = static::extract($data, $keys[$i]);
  444. }
  445. $out = array();
  446. $data = $extracted;
  447. $count = count($data[0]);
  448. if (preg_match_all('/\{([0-9]+)\}/msi', $format, $keys2) && isset($keys2[1])) {
  449. $keys = $keys2[1];
  450. $format = preg_split('/\{([0-9]+)\}/msi', $format);
  451. $count2 = count($format);
  452. for ($j = 0; $j < $count; $j++) {
  453. $formatted = '';
  454. for ($i = 0; $i <= $count2; $i++) {
  455. if (isset($format[$i])) {
  456. $formatted .= $format[$i];
  457. }
  458. if (isset($keys[$i]) && isset($data[$keys[$i]][$j])) {
  459. $formatted .= $data[$keys[$i]][$j];
  460. }
  461. }
  462. $out[] = $formatted;
  463. }
  464. return $out;
  465. }
  466. $count2 = count($data);
  467. for ($j = 0; $j < $count; $j++) {
  468. $args = array();
  469. for ($i = 0; $i < $count2; $i++) {
  470. if (isset($data[$i][$j])) {
  471. $args[] = $data[$i][$j];
  472. }
  473. }
  474. $out[] = vsprintf($format, $args);
  475. }
  476. return $out;
  477. }
  478. /**
  479. * Inserts `$data` into an array as defined by `$path`.
  480. *
  481. * @param mixed $list Where to insert into.
  482. * @param mixed $path A dot-delimited string.
  483. * @param array $data Data to insert.
  484. * @return array
  485. */
  486. public static function insert($list, $path, $data = array()) {
  487. if (!is_array($path)) {
  488. $path = explode('.', $path);
  489. }
  490. $_list =& $list;
  491. foreach ($path as $i => $key) {
  492. if (is_numeric($key) && intval($key) > 0 || $key === '0') {
  493. $key = intval($key);
  494. }
  495. if ($i === count($path) - 1) {
  496. $_list[$key] = $data;
  497. } else {
  498. if (!isset($_list[$key])) {
  499. $_list[$key] = array();
  500. }
  501. $_list =& $_list[$key];
  502. }
  503. }
  504. return $list;
  505. }
  506. /**
  507. * Checks to see if all the values in the array are numeric.
  508. *
  509. * @param array $array The array to check. If null, the value of the current Set object.
  510. * @return mixed `true` if values are numeric, `false` if not and `null` if the array to
  511. * check is empty.
  512. */
  513. public static function isNumeric($array = null) {
  514. if (empty($array)) {
  515. return null;
  516. }
  517. if ($array === range(0, count($array) - 1)) {
  518. return true;
  519. }
  520. $numeric = true;
  521. $keys = array_keys($array);
  522. $count = count($keys);
  523. for ($i = 0; $i < $count; $i++) {
  524. if (!is_numeric($array[$keys[$i]])) {
  525. $numeric = false;
  526. break;
  527. }
  528. }
  529. return $numeric;
  530. }
  531. /**
  532. * This function can be used to see if a single item or a given XPath
  533. * match certain conditions.
  534. *
  535. * @param array $data An array of data to execute the match on.
  536. * @param mixed $conditions An array of condition strings or an XPath expression.
  537. * @param integer $i Optional: The 'nth'-number of the item being matched.
  538. * @param integer $length
  539. * @return boolean
  540. */
  541. public static function matches($data = array(), $conditions, $i = null, $length = null) {
  542. if (!$conditions) {
  543. return true;
  544. }
  545. if (is_string($conditions)) {
  546. return (boolean) static::extract($data, $conditions);
  547. }
  548. foreach ($conditions as $condition) {
  549. if ($condition === ':last') {
  550. if ($i !== $length) {
  551. return false;
  552. }
  553. continue;
  554. } elseif ($condition === ':first') {
  555. if ($i !== 1) {
  556. return false;
  557. }
  558. continue;
  559. }
  560. if (!preg_match('/(.+?)([><!]?[=]|[><])(.*)/', $condition, $match)) {
  561. if (ctype_digit($condition)) {
  562. if ($i !== (int) $condition) {
  563. return false;
  564. }
  565. } elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) {
  566. return in_array($i, $matches[0]);
  567. } elseif (!isset($data[$condition])) {
  568. return false;
  569. }
  570. continue;
  571. }
  572. list(,$key,$op,$expected) = $match;
  573. if (!isset($data[$key])) {
  574. return false;
  575. }
  576. $val = $data[$key];
  577. if ($op === '=' && $expected && $expected{0} === '/') {
  578. return preg_match($expected, $val);
  579. } elseif ($op === '=' && $val != $expected) {
  580. return false;
  581. } elseif ($op === '!=' && $val == $expected) {
  582. return false;
  583. } elseif ($op === '>' && $val <= $expected) {
  584. return false;
  585. } elseif ($op === '<' && $val >= $expected) {
  586. return false;
  587. } elseif ($op === '<=' && $val > $expected) {
  588. return false;
  589. } elseif ($op === '>=' && $val < $expected) {
  590. return false;
  591. }
  592. }
  593. return true;
  594. }
  595. /**
  596. * This method can be thought of as a hybrid between PHP's `array_merge()`
  597. * and `array_merge_recursive()`. The difference to the two is that if an
  598. * array key contains another array then the function behaves recursive
  599. * (unlike `array_merge()`) but does not do if for keys containing strings
  600. * (unlike `array_merge_recursive()`). Please note: This function will work
  601. * with an unlimited amount of arguments and typecasts non-array parameters
  602. * into arrays.
  603. *
  604. * @param array $array1 The base array.
  605. * @param array $array2 The array to be merged on top of the base array.
  606. * @return array Merged array of all passed params.
  607. */
  608. public static function merge(array $array1, array $array2) {
  609. $args = array($array1, $array2);
  610. if (!$array1 || !$array2) {
  611. return $array1 ?: $array2;
  612. }
  613. $result = (array) current($args);
  614. while (($arg = next($args)) !== false) {
  615. foreach ((array) $arg as $key => $val) {
  616. if (is_array($val) && isset($result[$key]) && is_array($result[$key])) {
  617. $result[$key] = static::merge($result[$key], $val);
  618. } elseif (is_int($key)) {
  619. $result[] = $val;
  620. } else {
  621. $result[$key] = $val;
  622. }
  623. }
  624. }
  625. return $result;
  626. }
  627. /**
  628. * Normalizes a string or array list.
  629. *
  630. * @param mixed $list List to normalize.
  631. * @param boolean $assoc If `true`, `$list` will be converted to an associative array.
  632. * @param string $sep If `$list` is a string, it will be split into an array with `$sep`.
  633. * @param boolean $trim If `true`, separated strings will be trimmed.
  634. * @return array
  635. */
  636. public static function normalize($list, $assoc = true, $sep = ',', $trim = true) {
  637. if (is_string($list)) {
  638. $list = explode($sep, $list);
  639. $list = ($trim) ? array_map('trim', $list) : $list;
  640. return ($assoc) ? static::normalize($list) : $list;
  641. }
  642. if (!is_array($list)) {
  643. return $list;
  644. }
  645. $keys = array_keys($list);
  646. $count = count($keys);
  647. $numeric = true;
  648. if (!$assoc) {
  649. for ($i = 0; $i < $count; $i++) {
  650. if (!is_int($keys[$i])) {
  651. $numeric = false;
  652. break;
  653. }
  654. }
  655. }
  656. if (!$numeric || $assoc) {
  657. $newList = array();
  658. for ($i = 0; $i < $count; $i++) {
  659. if (is_int($keys[$i]) && is_scalar($list[$keys[$i]])) {
  660. $newList[$list[$keys[$i]]] = null;
  661. } else {
  662. $newList[$keys[$i]] = $list[$keys[$i]];
  663. }
  664. }
  665. $list = $newList;
  666. }
  667. return $list;
  668. }
  669. /**
  670. * Removes an element from an array as defined by `$path`.
  671. *
  672. * @param mixed $list From where to remove.
  673. * @param mixed $path A dot-delimited string.
  674. * @return array Array with `$path` removed from its value.
  675. */
  676. public static function remove($list, $path = null) {
  677. if (empty($path)) {
  678. return $list;
  679. }
  680. if (!is_array($path)) {
  681. $path = explode('.', $path);
  682. }
  683. $_list =& $list;
  684. foreach ($path as $i => $key) {
  685. if (is_numeric($key) && intval($key) > 0 || $key === '0') {
  686. $key = intval($key);
  687. }
  688. if ($i === count($path) - 1) {
  689. unset($_list[$key]);
  690. } else {
  691. if (!isset($_list[$key])) {
  692. return $list;
  693. }
  694. $_list =& $_list[$key];
  695. }
  696. }
  697. return $list;
  698. }
  699. /**
  700. * Sorts an array by any value, determined by a `Set`-compatible path.
  701. *
  702. * @param array $data
  703. * @param string $path A `Set`-compatible path to the array value.
  704. * @param string $dir Either `'asc'` (the default) or `'desc'`.
  705. * @return array
  706. */
  707. public static function sort($data, $path, $dir = 'asc') {
  708. $flatten = function($flatten, $results, $key = null) {
  709. $stack = array();
  710. foreach ((array) $results as $k => $r) {
  711. $id = $k;
  712. if (!is_null($key)) {
  713. $id = $key;
  714. }
  715. if (is_array($r)) {
  716. $stack = array_merge($stack, $flatten($flatten, $r, $id));
  717. } else {
  718. $stack[] = array('id' => $id, 'value' => $r);
  719. }
  720. }
  721. return $stack;
  722. };
  723. $extract = static::extract($data, $path);
  724. $result = $flatten($flatten, $extract);
  725. $keys = static::extract($result, '/id');
  726. $values = static::extract($result, '/value');
  727. $dir = ($dir === 'desc') ? SORT_DESC : SORT_ASC;
  728. array_multisort($values, $dir, $keys, $dir);
  729. $sorted = array();
  730. $keys = array_unique($keys);
  731. foreach ($keys as $k) {
  732. $sorted[] = $data[$k];
  733. }
  734. return $sorted;
  735. }
  736. /**
  737. * Slices an array into two, separating them determined by an array of keys.
  738. *
  739. * Usage examples:
  740. *
  741. * {{{ embed:lithium\tests\cases\util\SetTest::testSetSlice(1-4) }}}
  742. *
  743. * @param array $subject Array that gets split apart
  744. * @param array|string $keys An array of keys or a single key as string
  745. * @return array An array containing both arrays, having the array with requested keys first and
  746. * the remainder as second element
  747. */
  748. public static function slice(array $data, $keys) {
  749. $removed = array_intersect_key($data, array_fill_keys((array) $keys, true));
  750. $data = array_diff_key($data, $removed);
  751. return array($data, $removed);
  752. }
  753. }
  754. ?>