Skip to content

Commit

Permalink
Merge PR #121 from @niklaka - this adds support for multiple sort fie…
Browse files Browse the repository at this point in the history
…lds anda start selector to WireArray methods like find() and sort().
  • Loading branch information
ryancramerdesign committed Sep 23, 2012
1 parent 85aa779 commit ed28b1b
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 31 deletions.
129 changes: 99 additions & 30 deletions wire/core/Array.php
Expand Up @@ -656,17 +656,63 @@ public function removeAll() {
}

/**
* Sort this WireArray by the given property.
* Sort this WireArray by the given properties.
*
* You may also specify the $property as "property.subproperty", where property resolves to a Wire derived object,
* $properties can be given as a sortByField string, i.e. "name, datestamp" OR as an array of strings, i.e. array("name", "datestamp")
* You may also specify the properties as "property.subproperty", where property resolves to a Wire derived object,
* and subproperty resolves to a property within that object.
*
* @TODO Currently only sorts by one field at a time. Upgrade to support a sortByField string, i.e. "name, datestamp"
* @param string|array $properties Field names to sort by (comma separated string or an array). Prepend or append a minus "-" to reverse the sort (per field).
* @return WireArray reference to current instance.
*/
public function sort($properties) {
return $this->_sort($properties);
}

/**
* Sort this WireArray by the given properties (internal use)
*
* This function contains additions and modifications by @niklaka.
*
* @param string $property Field name to sort by. Prepend or append a minus "-" to reverse the sort.
* $properties can be given as a sortByField string, i.e. "name, datestamp" OR as an array of strings, i.e. array("name", "datestamp")
* You may also specify the properties as "property.subproperty", where property resolves to a Wire derived object,
* and subproperty resolves to a property within that object.
*
* @param string|array $properties Field names to sort by (comma separated string or an array). Prepend or append a minus "-" to reverse the sort (per field).
* @param int $numNeeded *Internal* amount of rows that need to be sorted (optimization used by filterData)
* @return WireArray reference to current instance.
*/
public function sort($property) {
protected function _sort($properties, $numNeeded = null) {

// string version is used for change tracking
$propertiesStr = is_array($properties) ? implode(',', $properties) : $properties;
if(!is_array($properties)) $properties = preg_split('/\s*,\s*/', $properties);

// shortcut for random (only allowed as the sole sort property)
// no warning/error for issuing more properties though
// TODO: warning for random+more properties (and trackChange() too)
if($properties[0] == 'random') return $this->shuffle();

$this->data = $this->stableSort($this, $properties, $numNeeded);

$this->trackChange("sort:$propertiesStr");

return $this;
}

/**
* Sort given array by first given property.
*
* This function contains additions and modifications by @niklaka.
*
* @param array &$data Reference to an array to sort.
* @param array $properties Array of properties: first property is used now and others in recursion, if needed.
* @param int $numNeeded *Internal* amount of rows that need to be sorted (optimization used by filterData)
* @return array Sorted array (at least $numNeeded items, if $numNeeded is given)
*/
protected function stableSort(&$data, $properties, $numNeeded = null) {

$property = array_shift($properties);

$unidentified = array();
$sortable = array();
Expand All @@ -678,14 +724,12 @@ public function sort($property) {
$property = trim($property, '-');
}

if($property == 'random') return $this->shuffle();

if($pos = strpos($property, ".")) {
$subProperty = substr($property, $pos+1);
$property = substr($property, 0, $pos);
}

foreach($this as $item) {
foreach($data as $item) {

$key = $this->getItemPropertyValue($item, $property);

Expand All @@ -707,43 +751,48 @@ public function sort($property) {

if(isset($sortable[$key])) {
// key resolved to the same value that another did, so keep them together by converting this index to an array
// this makes the algorithm stable (for equal keys the order would be undefined)
if(is_array($sortable[$key])) $sortable[$key][] = $item;
else $sortable[$key] = array($sortable[$key], $item);
} else {
$sortable[$key] = $item;
}
}

// sorth the items by the keys we collected
// sort the items by the keys we collected
if($reverse) krsort($sortable);
else ksort($sortable);

// add the items that resolved to no key to the end
foreach($unidentified as $item) $sortable[] = $item;
// add the items that resolved to no key to the end, as an array
$sortable[] = $unidentified;

// restore sorted array to lose sortable keys and restore proper keys
$a = array();
foreach($sortable as $key => $value) {
if(is_array($value)) {
// if more properties to sort by exist, use them for this sub-array
$n = null;
if($numNeeded) $n = $numNeeded - count($a);
if(count($properties)) $value = $this->stableSort($value, $properties, $n);
foreach($value as $k => $v) {
$newKey = $this->getItemKey($v);
$a[$newKey] = $v;
// are we done yet?
if($numNeeded && count($a) > $numNeeded) break;
}
} else {
$newKey = $this->getItemKey($value);
$a[$newKey] = $value;
}
// are we done yet?
if($numNeeded && count($a) > $numNeeded) break;
}

// overwrite the WireArray's data with the new sorted data
$this->data = $a;
$this->trackChange("sort:$property");

return $this;
return $a;
}

/**
* Get the vaule of $property from $item
* Get the value of $property from $item
*
* Used by the WireArray::sort method to retrieve a value from a Wire object.
* Primarily here as a template method so that it can be overridden.
Expand All @@ -762,6 +811,7 @@ protected function getItemPropertyValue(Wire $item, $property) {
* Filter out Wires that don't match the selector.
*
* This is applicable to and destructive to the WireArray.
* This function contains additions and modifications by @niklaka.
*
* @param string|Selectors $selectors AttributeSelector string to use as the filter.
* @param bool $not Make this a "not" filter? (default is false)
Expand All @@ -777,27 +827,46 @@ protected function filterData($selectors, $not = false) {
$selectors = new Selectors($selectors);
}

$sort = '';
$limit = '';
$sort = array();
$start = 0;
$limit = null;

foreach($this->data as $key => $item) {
// leave sort, limit and start away from filtering selectors
foreach($selectors as $selector) {
$remove = true;

foreach($selectors as $selector) {
if($selector->field === 'sort') {
// use all sort selectors
$sort[] = $selector->value;

if($selector->field === 'sort') {
$sort = $selector->value;
} else if($selector->field === 'start') {
// use only the last start selector
$start = (int) $selector->value;

} else if($selector->field === 'limit') {
$limit = (int) $selector->value;
} else if($selector->field === 'limit') {
// use only the last limit selector
$limit = (int) $selector->value;

} else if($not === $selector->matches($item)) {
unset($this->data[$key]);
}
} else {
// everything else is to be saved for filtering
$remove = false;
}

if($remove) $selectors->remove($selector);
}

// now filter the data according to the selectors that remain
foreach($this->data as $key => $item) {
foreach($selectors as $selector) {
if($not === $selector->matches($item)) {
unset($this->data[$key]);
}
}
}

if($sort) $this->sort($sort);
if($limit) while($this->count() > $limit) array_pop($this->data);
// if $limit has been given, tell sort the amount of rows that will be used
if(count($sort)) $this->_sort($sort, $limit ? $start+$limit : null);
if($start || $limit) $this->data = array_slice($this->data, $start, $limit, true);

$this->trackChange("filterData:$selectors");
return $this;
Expand Down
18 changes: 17 additions & 1 deletion wire/core/PageArray.php
Expand Up @@ -354,7 +354,23 @@ protected function filterData($selectors, $not = false) {
*
*/
protected function getItemPropertyValue(Wire $item, $property) {
return $item->getUnformatted($property);

if($item instanceof Page) {
$value = $item->getUnformatted($property);

} else if($item instanceof WireArray) {
if($property == 'count') {
$value = count($item);
} else {
$value = $item->first();
if($value) {
if($value instanceof Page) $value = $value->getUnformatted($property);
else if($value instanceof WireData) $value = $value->$property;
}
}
}

return $value;
}

/**
Expand Down

0 comments on commit ed28b1b

Please sign in to comment.