Go back library/Zend/Array.php
<?php
/*
* Zend_Array -- A fluent array wrapper for PHP.
*
* Version: 0.1, August 2009.
* Version: 0.2, January 2010.
* Version: 0.3, February 2010.
*
* Home page: http://chapman.id.au/fluid
*
* Copyright (c) 2009-10, Nigel Chapman <nigel@chapman.id.au>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of the Zend nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
class Zend_Array
{
protected $_array;
/*
* Instantiate with an array.
*
*/
public function __construct($array)
{
if (is_array($array)) {
$this->_array = $array;
} else {
$this->_array = array();
}
}
/*
* Initiators. These are factories that return a starting array,
* using something that meaningfully generates an array.
*
*/
static public function in($arr)
{
return new self($arr);
}
static public function take($arr)
{
return new self($arr);
}
static public function takeMultiple($ofSomething, $times)
{
return new self(array_fill(0, $times-1, $ofSomething));
}
static public function takeMatches($str, $pattern)
{
if (preg_match_all("/$pattern/", $str, $matches)) {
return new self($matches[0]);
} else {
return new self(array());
}
}
static public function takeWords($str, $pattern="\w+")
{
return self::takeMatches($str, $pattern);
}
static public function takeLines($str, $eolPattern="[\n\r]+")
{
return new self(split($eolPattern, $str));
}
static public function takeCharacters($str, $pattern=".")
{
return self::takeMatches($str, $pattern);
}
/*
* Replace this hack with str_getcsv when PHP 5.3 is generally adopted.
*
* @todo Move this to Zend_Array_File?
*
*/
static public function takeCsv($str)
{
$csv = array();
$fiveMBs = 5 * 1024 * 1024;
$fp = fopen("php://temp/maxmemory:$fiveMBs", 'r+');
fputs($fp, $str);
rewind($fp);
while ($data = fgetcsv($fp, 1000)) {
$csv[] = $data;
}
fclose($fp);
return new self($csv);
}
/*
* Terminators. Return something other than a Zend_Array object at the end
* of the chain. Simplest case is ->array(), which returns the current
* array.
*
* in($arr)-> ... ->out()
* take($arr)-> ... ->done()
*
*/
public function out()
{
return $this->_array;
}
public function done()
{
return $this->_array;
}
public function toArray()
{
return $this->_array;
}
public function dump()
{
die(var_dump($this->_array));
}
/*
* Instantiate a subclass of Zend_Array if found.
*
* NOTE: Assumes Zend_Loader!
*
*/
public function load($extension, $options=null)
{
$newClass = "Zend_Array_$extension";
return new $newClass($this->done(), $options);
}
/*
* Methods of this class (esp. get and collate) frequently need to get a
* certain named value from an object. We'll call these names 'get'
* statements.
*
* The get statement can be an anonymous function (or closure), string or
* array. If a function, it is called with the object as it's sole
* argument. If a string, is will be used as a key in an array, a property
* of an object, or a method of an object, in that order of precedence.
* Arrays are not handled here, see get().
*
*/
public function getFromObject($object, $get)
{
$out = null;
if (is_callable($get)) {
$out = $get($object);
} else if (is_string($get)) {
if (is_array($object)) {
if (isset($object[$get])) {
$out = $object[$get];
}
} else if (is_object($object)) {
if (isset($object->$get)) {
$out = $object->$get;
} else if (method_exists($object, $get)) {
$out = $object->$get();
}
}
}
return $out;
}
/*
* Return an array of a given key or attribute from each element...
*
* If an array of get sytatements is given, return an array of results.
*
*/
public function get($x, $else=null)
{
if (is_array($x)) {
$setOfGets = array();
foreach ($x as $get) {
$setOfGets[] = self::in($this->_array)->get($get, $else)->out();
}
return self::take($setOfGets)->flip();
} else {
$out = array();
foreach ($this->_array as $key => $object) {
if ($get = self::getFromObject($object, $x)) {
$out[$key] = $get;
} else if (!is_null($else)) {
$out[$key] = $else;
}
}
return new self($out);
}
}
/*
* Call a PHP function on each elementof the Zend_Array; return
* a Zend_Array of the results.
*
* @param string $functionName
* @access public
* @return Zend_Array
*/
public function php($functionName)
{
$out = array();
foreach ($this->_array as $key => $element) {
if (is_array($element)) {
$out[$key] = self::take($element)->php($functionName)->done();
} else {
$out[$key] = $functionName($element);
}
}
return new self($out);
}
/*
* Count elements. It's tempting to make something like
* count_array_values; I think better UNIX style to implement generic
* structural transformations (e.g. get all nodes at depth n; trim tree to
* depth z), then apply count/sum/whatever to the result.
*
*/
public function count()
{
return count($this->_array);
}
/*
* Terminator: sum array values.
*
*/
public function sum($get=null)
{
if (is_null($get)) {
return array_sum($this->_array);
} else {
return self::in($this->_array)->get($get)->sum();
}
}
public function average($get=null)
{
$count = $this->count();
if ($count == 0) {
return null;
} else {
return $this->sum($get) / $count;
}
}
/*
* Terminator: find minimum.
*
*/
public function min($get=null)
{
if (is_null($get)) {
return min($this->_array); // Works on arrays.
} else {
return self::in($this->_array)->get($get)->min();
}
}
/*
* Terminator: find maximum.
*
*/
public function max($get=null)
{
if (is_null($get)) {
return max($this->_array); // Works on arrays.
} else {
return self::in($this->_array)->get($get)->max();
}
}
/*
* Terminator: find standard deviation.
*
*/
public function stdDev($get=null)
{
if (is_null($get)) {
$average = self::take($this->_array)->average();
foreach ($this->_array as $value) {
$variance[] = pow($value - $average, 2);
}
return sqrt(self::take($variance)->average());
} else {
return self::in($this->_array)->get($get)->stdDev();
}
}
/*
* String functions assume a one-dimensional array.
*
*/
public function join($separator=' ', $final=null)
{
$n = count($this->_array);
$finalSeparator = (is_null($final)) ? $separator : $final;
if ($n < 3) {
return join($separator, $this->_array);
} else {
$last = array_pop($this->_array);
return join($separator, $this->_array).$finalSeparator.$last;
}
}
/*
* Echo strings one per line (for console use).
*
*/
public function joinLines()
{
return join("\n", $this->_array);
}
/*
* Use the current array (see ::get(array('x', 'y', 'z'))), as the
* arguments for sprintf. e.g. echo self()...join("\n");
*
*/
public function sprintf($pattern)
{
$output = array();
foreach ($this->_array as $key => $value) {
$output[] = vsprintf($pattern, $value);
}
return new self($output);
}
public function grep($regex)
{
if (substr($regex, 0, 1) == '/') {
// echo "$regex";
return new self(preg_grep($regex, $this->_array));
} else {
// echo '/'.preg_quote($regex).'/';
return new self(preg_grep('/'.preg_quote($regex).'/', $this->_array));
}
}
public function regex($regex, $replace)
{
if (is_callable($replace)) {
return new self(preg_replace_callback($regex, $replace, $this->_array));
} else {
return new self(preg_replace($regex, $replace, $this->_array));
}
}
public function wrap($before, $after=null)
{
if ($after == null) {
$after = $this->_closingTag($before); // Assume HTML
}
$out = array();
foreach ($this->_array as $key => $value) {
$out[$key] = $before.$value.$after;
}
return new self($out);
}
/**
* Simple string replace, but will use callables if given.
*
* @param mixed $swap
* @param string|function $with // ??
* @access public
* @return void
*/
public function replace($swap, $with)
{
$command = 'str_replace';
if (is_callable($swap)) {
$swapArgument = $this->get($swap)->done();
} else if (is_array($swap)) {
$swapArgument = $swap;
} else if (is_string($swap)) {
if (substr($swap, 0, 1) == '/') {
$command = 'preg_replace';
$swapArgument = $swap;
} else {
$swapArgument = $swap;
}
} else {
throw new Exception(__CLASS__."::".__METHOD__."() requires a string, array or callable argument.");
}
if (is_callable($with)) {
if ($command == 'preg_replace') {
$command = 'preg_replace_callback';
$withArgument = $with;
} else {
$withArgument = self::take($swapArgument)->map($with)->done();
}
} else if (is_string($with)) {
$withArgument = $with;
} else {
throw new Exception(__CLASS__."::".__METHOD__."() requires a string or callable argument.");
}
$output = array();
foreach ($this->_array as $str) {
$output[] = $command($swapArgument, $withArgument, $str);
}
return self::take($output);
}
/**
* Apply HTML highlighting to a string.
*
* @param mixed $get
* @param string $tag
* @access public
* @return void
*/
public function highlight($get, $tag="<i>")
{
foreach ($this->_array as $key => $value) {
if (is_callable($get)) {
$find = $get($value);
} else if (is_string($get)) {
$find = $get;
} else {
throw new Exception(__CLASS__."::".__METHOD__."() requires a strin or callable argument.");
}
$pattern = '/('.preg_quote($find).')/i';
$replace = $tag.'$1'.$this->_closingTag($tag);
$out[] = preg_replace($pattern, $replace, $value);
}
return new self($out);
}
/*
* Format any-dimensional array into an HTML ol/ul list.
*
*/
public function htmlList($indentDepth=0, $tag='ul', $escape=false)
{
$indent = str_repeat(' ', $indentDepth);
$html = '';
if (count($this->_array) > 0) {
$html .= "$indent<$tag>\n";
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
$html .= self::take($value)->htmlList($indentDepth + 1, $tag, $escape);
} else if ($escape == true) {
$html .= "$indent<li>".htmlspecialchars($value)."</li>\n";
} else {
$html .= "$indent<li>$value</li>\n";
}
}
$html .= "$indent</$tag>\n";
}
return $html;
}
/*
* Format 2-dimensional array into an HTML table.
*
* @todo Decide whether to make a key expander so all child arrays/objects
* have the same keys/attributes; could have more general applications.
*
*/
public function htmlTable()
{
$html = "<table>\n";
$html .= $this->htmlTableRows();
$html .= "</table>\n";
return $html;
}
public function htmlTableRows()
{
$html = "";
foreach ($this->_array as $r => $row) {
$html .= "<tr>\n";
foreach ($row as $c => $cell) {
$html .= " <td>$cell</td>\n";
}
$html .= "</tr>\n";
}
return $html;
}
/**
* return the current _array as a JSON encoded string.
*
* @access public
* @return void
*/
public function json()
{
return json_encode($this->_array);
}
/*
* Generic processors
*
*/
/*
* Recurse across the array and run a given function on all non-array
* values.
*
* @todo TEST
*
*/
public function walk($function, $data=null)
{
if (is_callable($function)) {
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
self::take($value)->walk($function, $data);
} else {
$function($value, $data);
}
}
return new self($output);
} else {
throw new Exception(
__CLASS__."::".__METHOD__."() requires a callable argument."
);
}
}
/*
* Recursively replace all non-array values with the result of a given
* callable.
*
*/
public function map($function, $data=null)
{
if (is_callable($function)) {
$output = array();
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
$output[$key] = self::in($value)->map($function, $data)->out();
} else {
$output[$key] = $function($value, $data);
}
}
return new self($output);
} else {
throw new Exception(__CLASS__."::".__METHOD__."() requires a callable argument.");
}
}
/*
* Recursively call a given method on every element of an array; may be
* best used with self::filterClass('Some_Object') to ensure they are the
* right kind of object.
*
* @todo Pass multiple arguments to user-created function
*
*/
public function call($function, $argument=null)
{
$output = array();
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
$output[$key] = self::take($value)
->call($function, $argument)
->done();
} else {
$output[$key] = $value->$function($value, $argument);
}
}
return new self($output);
}
/*
* @todo Pass user arguments to PHP function WITHOUT using
*
*/
/*
* Recursively reconstruct an array, but with only the entries returning
* true for a certain filtering function.
*
* @todo Prune empty arrays afterward?
*
*/
public function filter($function)
{
if (is_callable($function)) {
$output = array();
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
$output[$key] = self::take($value)->filter($function)->done();
} else {
if ($function($value) == true) {
$output[$key] = $value;
}
}
}
return new self($output);
} else {
throw new Exception(
__CLASS__."::".__METHOD__."() requires a callable argument."
);
}
}
/*
* Ordering and limiting functions.
* ____________________________________________________________
*
* ->sort()
* ->sort(null, int SORT_ASC|SORT_DESC)
* ->sort($get=null, int SORT_ASC|SORT_DESC) // Can be a function
* ->sort(array $getStatements)
*
* ->ksort() // Same, but sorting keys
* ->usort($cmp) // With a user-specified cmp(-1,0,+1) function
* [NC 2009-08-08] I don't think I can manage the holy grail at present --
* combining a get operation with a custom comparator; one requires
* array_multisort and the other requires some variant of usort.
*
* Because PHP's sorting uses quicksort, which is stable, multikey sorts
* are the same as the individual sorts done in reverse order. reverse
* for sorting on multiple keys. So sort('age', 'name') is the same thing
* as sort('name')->sort('age').
*
* @todo add a separate naturalSort() for numeric ordering.
*
*
*/
public function sort($get=null)
{
if (is_array($get)) {
$sorted = self::take($this->_array);
foreach (array_reverse($get) as $eachGet) {
$sorted = $sorted->sort($eachGet);
}
return $sorted;
} else if (is_null($get)) {
$sortingOrder = $this->_array;
} else {
// Check for reverse sorting
if (substr($get, 0, 1) == '-') {
$get = substr($get, 1);
$sortingOrder = $this->get($get)->reverse()->out();
} else {
$sortingOrder = $this->get($get)->out();
}
}
// foreach ($output as $key => $value) { // Recurse
// if (is_array($value)) {
// $output[$key] = self::in($value)->sort($get, $cmp)->out();
// }
// }
$output = $this->_array;
if (count($sortingOrder) != count($output)) {
die(var_dump($get). ' | ' . var_dump($sortingOrder). ' | '. var_dump($output));
}
array_multisort($sortingOrder, $output);
return new self($output);
}
/*
* @todo Add natural-number sorting; really need this for integer values.
*
*/
public function sortNatural()
{
// ..
}
public function unique()
{
return new self(array_unique($this->_array));
}
/*
* Slicing processors
*
* If first or last are given an argument, they return a fluid array
* containing that number of elements (or as many as possible); If no
* argument of given, they return the first or last element respectively
* (i.e. act as a terminator in the chain).
*
*/
public function slice($position, $length=null)
{
return new self(array_slice($this->_array, $position, $length));
}
public function first($n=null)
{
if ($n == null) {
return reset($this->_array);
} else {
return new self(array_slice($this->_array, 0, $n));
}
}
public function last($n)
{
if ($n == null) {
return end($this->_array);
} else {
return new self(array_slice($this->_array, -$n));
}
}
/*
* Shuffle the list; pick random entries.
*
*/
public function shuffle()
{
return new self(shuffle($this->_array));
}
/*
* Following PHP's array_rand, ->random() equals ->random(1), and acts as a
* terminator. Higher integers return arrays with that many elements
* (or as many as are availeble).
*
*/
public function random($n=1)
{
if (is_int($n)) {
if ($n > 1) {
$values = array();
foreach (array_rand($this->_array, $n) as $key) {
$values[] = $this->_array[$key];
}
return new self($values);
} else if ($n == 1) {
return $this->_array[array_rand($this->_array)];
}
// else...
}
throw new Exception(
__CLASS__."::".__METHOD__."() requires a positive integer argument."
);
}
/*
* Segment an array into a certain number of parts.
*
*/
public function segment($parts)
{
$partLength = ceil($this->count() / $parts);
return new self(array_chunk($this->_array, $partLength, $preserveKeys=true));
}
/*
* Flip a two-dimensional array diagonally. Not to be confused with PHP's
* array_flip, which inverts the key => value relation.
*
*/
public function flip()
{
$out = array();
$r = 0;
foreach ($this->_array as $row) {
$c = 0;
foreach ($row as $cell) {
if (isset($out[$c])) {
$out[$c][] = $cell;
} else {
$out[$c] = array($cell);
}
$c += 1;
}
$r += 1;
}
return new self($out);
}
/*
* Organize a one-dimensional array into columns.
*
*/
public function columns($n)
{
return self::take($this->_array)->segment($n)->flip();
}
/*
* Collations are performed on one-dimensional arrays; they group records
* together for easy multi-layered looping. e.g.
* $loop = Zend_Array::take($addresses)
* ->collate(array('state', 'suburb'))
* ->out();
* ksort($loop);
* foreach ($loop as $state => $subLoop) {
* echo "<h1>$state</h1>\n";
* ksort($subLoop);
* foreach ($subLoop as $suburb => $addressList) {
* echo "<h2>$suburb</h2>\n";
* echo Zend_Array::take($addressList)
* ->sort('street')
* ->call('format') // Method
* ->htmlList();
* }
* }
* @todo Decide if ksort should be a collation option.
*
*/
public function collate(array $getStatements, $depth=0)
{
$getNow = reset(array_slice($getStatements, 0, 1));
$getLater = array_slice($getStatements, 1);
$collation = array();
foreach ($this->_array as $key => $value) {
$index = self::getFromObject($value, $getNow);
if (!is_null($index)) {
if (isset($collation[$index])) {
$collation[$index][] = $value;
} else {
$collation[$index] = array($value);
}
}
}
if (count($getLater) > 0) {
foreach ($collation as $key => $value) {
$collation[$key] = self::in($value)
->collate($getLater)
->out();
}
}
return new self($collation);
}
/*
* @todo Improve this if performance becomes an issue; it's O(n log n);
* could easily, though not as elegantly, be O(n).
*
*/
public function flatten()
{
$newList = array();
foreach ($this->_array as $object) {
if (is_array($object)) {
$subList = self::in($object)->flatten()->out();
foreach ($subList as $item) {
$newList[] = $item;
}
} else {
$newList[] = $object;
}
}
return new self($newList);
}
/*
* Various wrappers for PHP array functions
* ____________________________________________________________
*
*/
public function reverse()
{
$output = array();
foreach ($this->_array as $key => $value) {
if (is_array($value)) {
$output[$key] = self::take($value)->reverse()->out();
} else {
$output[$key] = $value;
}
}
return new self(array_reverse($output));
}
/*
*
* Support functions
*
*/
/**
* Find the closingTag to match a given opening tag.
*
* @param mixed $openingTag
* @static
* @access public
* @return void
*/
protected function _closingTag($openingTag)
{
return preg_replace("/^\<([A-Za-z0-9]+)[^>]*\>/", "</\\1>", $openingTag);
}
protected function _getRandomId()
{
$i = rand(10000000,90000000);
return 'random-'.$i;
}
}
Go back library/Zend/Array.php