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 1024 1024;
        
$fp fopen("php://temp/maxmemory:$fiveMBs"'r+');
        
fputs($fp$str);
        
rewind($fp);
        while (
$data fgetcsv($fp1000)) {
            
$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 $average2);
            }
            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($regex01) == '/') { 
            
//  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($swap01) == '/') {  
                
$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($get01) == '-') {
                
$get substr($get1);
                
$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->_array0$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($getStatements01));
        
$getLater array_slice($getStatements1);
        
$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