<?php
/**
* List comprehensions for PHP (php-lc)
* @version 0.13
* @author Vlad Andersen <hide@address.com>
* @link http://code.google.com/p/php-lc/
* @license GPL
*
* With php-lc you can easily manipulate your PHP arrays in style of Python list comprehensions.
*
* Python syntax: [i*2 for i in Data if i > 5]
* php-lc syntax: lc ('$i*2 for $i in $Data if $i > 5', compact ('Data'))
*
* See what you can do with php-lc on: http://code.google.com/p/php-lc/
*/
if (!function_exists('lc')) {
/**
* lc() - a shortcut for executing a list comprehension
*
* == Basic syntax ==
*
* The syntax for php-lc expressions is the following:
*
* <return> for [<key> => ]<element> in <Data> [if <condition>]
*
* <return> could be any expression that is using <element>, <key> (if provided, discussed below)
* or any of the passed variables. If no <key> is provided, php-lc will return an array with consecutive
* numeric indexes (a list).
*
* == Python-style formatting
*
* You can easily format string in Python style by prefixing the <return> with a percent sign (%).
*
* %<a href="/data/%s/">%s</a> % strtolower ($value), $value for $value in $Data
*
* == Returning hashes ==
*
* You can also use special syntax of <return>:
*
* {substr ($value, 0, 1) => $value} for $value in $Data
*
* Using {$x => $y} as <return> will return an array with keyed indexes (a hash).
*
* Note: you can use a shortcut of the hash return syntax (without the curly braces) if you do not have any
* array declarations in your code.
*
* E.g., you can use:
*
* substr ($value, 0, 1) => $value for $value in $Data
*
* with the same result as above. However, if you have any array declarations in your return code (or even
* the string 'array') please use the full syntax:
*
* { $value => array ($value) } for $value in Data
*
* Also please note that you can use only scalar values as keys if you choose to return hashes.
*
* You can also user the following code:
*
* substr ($value, 0, 1) => [$value] for $value in $Data
*
* To create hashes with arrays (lists) of elements (e.g., this code will group values according to its first
* letter).
*
* <key> and <element> should be regular PHP variables that you'll be using in <return> and <condition>.
* When cycling through <Data>, each time <key> will be assigned to the current key value, <element> will be
* assigned to the current element. <key> is optional.
*
* == Other variables ==
*
* <Data> is the name of variable passed to the list comprehension (discussed below).
*
* <condition> could be any expression that is using <element>, <key> or any of the passed variables.
* Should return a boolean value. <condition> is optional.
*
* == Passing variables to php-lc ==
*
* The second argument of the function is a hash of variables that are passed into the list comprehension:
*
* array ('Data' => $Data)
* array ('Req' => $_REQUEST, 'Get' => $_GET, 'Post' => $_POST, 'serverArgCount' => count($_SERVER))
*
* The variables are known inside the list comprehension by the keys you've given them in the Data array.
*
* A shorthand for writing:
*
* array ('Data' => $Data, 'foo' => $foo)
*
* is:
*
* compact ('Data', 'foo')
*
* @param string $expression List comprehension expression
* @param array $Data List comprehension variables
* @return array
*/
function lc($expression, $Data = array()) {
return ListComprehension::execute($expression, $Data);
}
}
/** == Tests are included as doctests, see http://code.google.com/p/testing-doctest/ ==
*
* <code>
* // doctest: Simple arrays
* // flags: NORMALIZE_WHITESPACE
* $Foo = array (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
* print_r (lc ('$i*2 for $i in $Foo if $i > 5', compact ('Foo')));
* // expects:
* // Array
* // (
* // [0] => 12
* // [1] => 14
* // [2] => 16
* // [3] => 18
* // [4] => 20
* // )
* </code>
*
* <code>
* // doctest: String operations
* // flags: NORMALIZE_WHITESPACE
* $Foo = array ('Alabama', 'California', 'Texas', 'New York');
* print_r (lc ('strtoupper($state) for $state in $Foo if strlen ($state) > 5 and strlen ($state) < 9', array ('Foo' => $Foo)));
* // expects:
* // Array
* // (
* // [0] => ALABAMA
* // [1] => NEW YORK
* // )
* </code>
*
* <code>
* // doctest: String hashes
* // flags: NORMALIZE_WHITESPACE
* $Foo = array ('AL' => 'Alabama', 'CA' => 'California', 'TE' => 'Texas', 'NY' => 'New York');
* print_r (lc ('{strtoupper($state) => strtolower ($code)} for $code => $state in $Foo if strlen ($state) > 5 and strlen ($state) < 9', array ('Foo' => $Foo)));
* // expects:
* // Array
* // (
* // [ALABAMA] => al
* // [NEW YORK] => ny
* // )
* </code>
*
* <code>
* // doctest: Hashes and regular expressions
* // flags: NORMALIZE_WHITESPACE
* $Foo = array (
* array ('name' => 'David Beckham', 'number' => 7),
* array ('name' => 'Ronaldinho', 'number' => 10),
* array ('name' => 'David Villa', 'number' => 9)
* );
* print_r (lc ('$player["number"] => preg_replace (\'#^[A-z]+\s#i\', "", $player["name"]) for $player in $Foo', array ('Foo' => $Foo)));
* // expects:
* // Array
* // (
* // [7] => Beckham
* // [10] => Ronaldinho
* // [9] => Villa
* // )
* </code>
*
* <code>
* // doctest: Short syntax for sprintf
* // flags: NORMALIZE_WHITESPACE
* $Foo = array (
* array ('title' => 'Brad Pitt', 'id' => "nm0000093"),
* array ('title' => 'Al Pacino', 'id' => "nm0000199"),
* array ('title' => 'Keanu Reeves', 'id' => "nm0000206")
* );
* print_r (lc ('%<a href="http://www.imdb.com/%s">%s</a> % $actor["id"], $actor["title"] for $actor in $Foo', array ('Foo' => $Foo)));
* // expects:
* // Array
* // (
* // [0] => <a href="http://www.imdb.com/nm0000093">Brad Pitt</a>
* // [1] => <a href="http://www.imdb.com/nm0000199">Al Pacino</a>
* // [2] => <a href="http://www.imdb.com/nm0000206">Keanu Reeves</a>
* // )
* </code>
*
* <code>
* // doctest: Syntax for arrays inside arrays ([])
* // flags: NORMALIZE_WHITESPACE
* $Foo = array (
* array ('name' => 'Beckham', 'number' => 7),
* array ('name' => 'Ronaldinho', 'number' => 10),
* array ('name' => 'Arshavin', 'number' => 10),
* array ('name' => 'Raul', 'number' => 7),
* array ('name' => 'Villa', 'number' => 9)
* );
* print_r (lc ('$player["number"] => [$player["name"]] for $player in $Foo', array ('Foo' => $Foo)));
* // expects:
* // Array
* // (
* // [7] => Array
* // (
* // [0] => Beckham
* // [1] => Raul
* // )
* // [10] => Array
* // (
* // [0] => Ronaldinho
* // [1] => Arshavin
* // )
* // [9] => Array
* // (
* // [0] => Villa
* // )
* // )
* </code>
*
*/
class ListComprehension {
private $expression = '';
private $Variables = array();
const GLOBAL_ID = '__listcomprehension';
/**
* See description of the lc function above.
*
* @param string $expression List comprehension expression
* @param array $Data List comprehension variables
* @return array
*/
public static function execute ($expression, $Data = array()) {
$ListComprehension = new self ($expression, $Data);
return $ListComprehension->evaluate();
}
private function __construct ($expression, $Data = array()) {
$this->expression = $expression;
$this->Variables = $Data;
}
private function evaluate() {
$Object = array(); $IteratorMatches = array(); $ReturnMatches = array();
if (!preg_match('#^\s*(.+?)\s+for\s+(.+?)\s+in\s+([^\[\]]+?)(\s+if\s+(.+?))?\s*$#ims', $this->expression, $Object)) return false;
// print_r ($Object);
$LCObject = new ListComprehensionObject();
$LCObject->Iterable = $this->Variables[ltrim($Object[3], '$')];
$LCObject->condition = isset ($Object[5]) ? $Object[5] : true;
$LCObject->Variables = $this->Variables;
$LCObject->iteratorName = $Object[2];
if (preg_match ('#(.+)=>(.+)#', $LCObject->iteratorName, $IteratorMatches)) {
$LCObject->iteratorName = trim ($IteratorMatches[2]);
$LCObject->iteratorKeyName = trim ($IteratorMatches[1]);
}
$LCObject->return = trim($Object[1]);
if (preg_match ('#^%(.+) % (.+)$#', $LCObject->return, $Matches)) {
$LCObject->return = "sprintf ('" . $Matches[1] . "', " . $Matches[2] . ')';
}
if (preg_match ('#=>#', $LCObject->return)) {
if (!preg_match ('#array#i', $LCObject->return)) {
$LCObject->return = sprintf ('{%s}', trim ($LCObject->return, '{}'));
}
if (preg_match ('#^{(.+?)=>(.+)}$#', $LCObject->return, $ReturnMatches)) {
$LCObject->return = trim ($ReturnMatches[2]);
$LCObject->returnKey = trim ($ReturnMatches[1]);
}
if (preg_match ('#^\[.+\]$#', $LCObject->return)) {
$LCObject->return = substr ($LCObject->return, 1, -1);
$LCObject->optionReturnArrayInHash = true;
}
}
return $LCObject->run();
}
}
/**
* The class for running list comprehension objects. Do not call directly.
*/
class ListComprehensionObject {
public $Iterable = array();
public $condition = '';
public $Variables = array();
public $iteratorName;
public $iteratorKeyName = '';
public $return;
public $returnKey = '';
/**
* @var bool True if syntax of $k => [$v] used.
*/
public $optionReturnArrayInHash = false;
private $currentIterator;
public function run() {
if (!is_array ($this->Iterable)) return array();
$GLOBALS[ListComprehension::GLOBAL_ID] = $this->Variables;
//print $filterExpression;
if (!$this->iteratorKeyName) {
$filterExpression = 'extract ($GLOBALS["'.ListComprehension::GLOBAL_ID.'"]); return (' . $this->condition . ');';
$filterFunction = create_function($this->iteratorName, $filterExpression);
$this->Iterable = array_filter($this->Iterable, $filterFunction);
} else {
$filterExpression = 'extract ($GLOBALS["'.ListComprehension::GLOBAL_ID.'"]); if (' . $this->condition . ') return array (' .
$this->iteratorKeyName . ' => ' . $this->iteratorName . ');';
$filterFunction = create_function($this->iteratorKeyName . ',' . $this->iteratorName, $filterExpression);
$this->Iterable = array_map($filterFunction, array_keys($this->Iterable), $this->Iterable);
$this->Iterable = array_filter ($this->Iterable);
$Data = array();
foreach ($this->Iterable as $Arr)
$Data[key($Arr)] = current($Arr);
$this->Iterable = $Data;
}
if (!$this->returnKey) {
$returnExpression = 'extract ($GLOBALS["'.ListComprehension::GLOBAL_ID.'"]); return (' . $this->return . ');';
} else {
$returnExpression = 'extract ($GLOBALS["'.ListComprehension::GLOBAL_ID.'"]); return array (' . $this->returnKey . ' => ' . $this->return . ');';
}
if (!$this->iteratorKeyName) {
$returnFunction = create_function($this->iteratorName, $returnExpression);
if (!is_callable($returnFunction))
die ("Failed to execute the following lc-expression: " . $returnExpression);
$this->Iterable = array_map($returnFunction, $this->Iterable);
} else {
$returnFunction = create_function($this->iteratorKeyName . ',' . $this->iteratorName, $returnExpression);
$this->Iterable = array_map($returnFunction, array_keys($this->Iterable), $this->Iterable);
}
unset ($GLOBALS[ListComprehension::GLOBAL_ID]);
if (!$this->returnKey)
return array_values($this->Iterable);
$Data = array();
foreach ($this->Iterable as $Arr) {
if (!$this->optionReturnArrayInHash) {
$Data[key($Arr)] = current($Arr);
continue;
}
// If option return array in hash.
if (!isset ($Data[key($Arr)]))
$Data[key($Arr)] = array();
$Data[key($Arr)][] = current($Arr);
}
$this->Iterable = $Data;
return $this->Iterable;
}
}