From 0e89c4b6cd855f68b5c6ca99e96e261e65420457 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Sat, 16 Nov 2013 13:05:30 +1300 Subject: [PATCH] API: Implement SS_Map::push() to append values. FIX: SS_Map::count() not taking into account additional firstItems --- model/Map.php | 330 +++++++++++++++++++++++++++++++++------- tests/model/MapTest.php | 181 ++++++++++++++++++++-- 2 files changed, 442 insertions(+), 69 deletions(-) diff --git a/model/Map.php b/model/Map.php index c2d3a15cc..2d4ca6b08 100644 --- a/model/Map.php +++ b/model/Map.php @@ -7,12 +7,26 @@ * @subpackage model */ class SS_Map implements ArrayAccess, Countable, IteratorAggregate { + protected $list, $keyField, $valueField; + /** + * @see SS_Map::unshift() + * + * @var array $firstItems + */ protected $firstItems = array(); + /** + * @see SS_Map::push() + * + * @var array $lastItems + */ + protected $lastItems = array(); + /** * Construct a new map around an SS_list. + * * @param $list The list to build a map from * @param $keyField The field to use as the key of each map entry * @param $valueField The field to use as the value of each map entry @@ -24,182 +38,355 @@ class SS_Map implements ArrayAccess, Countable, IteratorAggregate { } /** - * Set the key field for this map + * Set the key field for this map. + * + * @var string $keyField */ public function setKeyField($keyField) { $this->keyField = $keyField; } /** - * Set the value field for this map + * Set the value field for this map. + * + * @var string $valueField */ public function setValueField($valueField) { $this->valueField = $valueField; } /** - * Return an array equivalent to this map + * Return an array equivalent to this map. + * + * @return array */ public function toArray() { $array = array(); + foreach($this as $k => $v) { $array[$k] = $v; } + return $array; } /** - * Return all the keys of this map + * Return all the keys of this map. + * + * @return array */ public function keys() { - $output = array(); - foreach($this as $k => $v) { - $output[] = $k; - } - return $output; + return array_keys($this->toArray()); } /** - * Return all the values of this map + * Return all the values of this map. + * + * @return array */ public function values() { - $output = array(); - foreach($this as $k => $v) { - $output[] = $v; - } - return $output; + return array_values($this->toArray()); } /** - * Unshift an item onto the start of the map + * Unshift an item onto the start of the map. + * + * Stores the value in addition to the {@link DataQuery} for the map. + * + * @var string $key + * @var mixed $value */ public function unshift($key, $value) { $oldItems = $this->firstItems; - $this->firstItems = array($key => $value); - if($oldItems) $this->firstItems = $this->firstItems + $oldItems; + $this->firstItems = array( + $key => $value + ); + + if($oldItems) { + $this->firstItems = $this->firstItems + $oldItems; + } + + return $this; + } + + /** + * Pushes an item onto the end of the map. + * + * @var string $key + * @var mixed $value + */ + public function push($key, $value) { + $oldItems = $this->lastItems; + + $this->lastItems = array( + $key => $value + ); + + if($oldItems) { + $this->lastItems = $this->lastItems + $oldItems; + } return $this; } // ArrayAccess + /** + * @var string $key + * + * @return boolean + */ public function offsetExists($key) { - if(isset($this->firstItems[$key])) return true; + if(isset($this->firstItems[$key])) { + return true; + } + + if(isset($this->lastItems[$key])) { + return true; + } $record = $this->list->find($this->keyField, $key); + return $record != null; } + + /** + * @var string $key + * + * @return mixed + */ public function offsetGet($key) { - if(isset($this->firstItems[$key])) return $this->firstItems[$key]; + if(isset($this->firstItems[$key])) { + return $this->firstItems[$key]; + } + + if(isset($this->lastItems[$key])) { + return $this->lastItems[$key]; + } $record = $this->list->find($this->keyField, $key); + if($record) { $col = $this->valueField; - return $record->$col; - } else { - return null; - } - } - public function offsetSet($key, $value) { - if(isset($this->firstItems[$key])) return $this->firstItems[$key] = $value; - user_error("SS_Map is read-only", E_USER_ERROR); + return $record->$col; + } + + return null; } + + /** + * Sets a value in the map by a given key that has been set via + * {@link SS_Map::push()} or {@link SS_Map::unshift()} + * + * Keys in the map cannot be set since these values are derived from a + * {@link DataQuery} instance. In this case, use {@link SS_Map::toArray()} + * and manipulate the resulting array. + * + * @var string $key + * @var mixed $value + */ + public function offsetSet($key, $value) { + if(isset($this->firstItems[$key])) { + return $this->firstItems[$key] = $value; + } + + if(isset($this->lastItems[$key])) { + return $this->lastItems[$key] = $value; + } + + user_error( + "SS_Map is read-only. Please use $map->push($key, $value) to append values", + E_USER_ERROR + ); + } + + /** + * Removes a value in the map by a given key which has been added to the map + * via {@link SS_Map::push()} or {@link SS_Map::unshift()} + * + * Keys in the map cannot be unset since these values are derived from a + * {@link DataQuery} instance. In this case, use {@link SS_Map::toArray()} + * and manipulate the resulting array. + * + * @var string $key + * @var mixed $value + */ public function offsetUnset($key) { if(isset($this->firstItems[$key])) { unset($this->firstItems[$key]); + return; } - user_error("SS_Map is read-only", E_USER_ERROR); + if(isset($this->lastItems[$key])) { + unset($this->lastItems[$key]); + + return; + } + + user_error( + "SS_Map is read-only. Unset cannot be called on keys derived from the DataQuery", + E_USER_ERROR + ); } - // IteratorAggreagte - + /** + * Returns an SS_Map_Iterator instance for iterating over the complete set + * of items in the map. + * + * Satisfies the IteratorAggreagte interface. + * + * @return SS_Map_Iterator + */ public function getIterator() { - return new SS_Map_Iterator($this->list->getIterator(), $this->keyField, $this->valueField, $this->firstItems); + return new SS_Map_Iterator( + $this->list->getIterator(), + $this->keyField, + $this->valueField, + $this->firstItems, + $this->lastItems + ); } - // Countable - + /** + * Returns the count of items in the list including the additional items set + * through {@link SS_Map::push()} and {@link SS_Map::unshift}. + * + * @return int + */ public function count() { - return $this->list->count(); + return $this->list->count() + + count($this->firstItems) + + count($this->lastItems); } } /** * Builds a map iterator around an Iterator. Called by SS_Map + * * @package framework * @subpackage model */ class SS_Map_Iterator implements Iterator { + protected $items; protected $keyField, $titleField; protected $firstItemIdx = 0; + + protected $endItemIdx; + protected $firstItems = array(); + protected $lastItems = array(); + protected $excludedItems = array(); /** - * @param $items The iterator to build this map from - * @param $keyField The field to use for the keys - * @param $titleField The field to use for the values - * @param $fistItems An optional map of items to show first + * @param Iterator $items The iterator to build this map from + * @param string $keyField The field to use for the keys + * @param string $titleField The field to use for the values + * @param array $fristItems An optional map of items to show first + * @param array $lastItems An optional map of items to show last */ - public function __construct(Iterator $items, $keyField, $titleField, $firstItems = null) { + public function __construct(Iterator $items, $keyField, $titleField, $firstItems = null, $lastItems = null) { $this->items = $items; $this->keyField = $keyField; $this->titleField = $titleField; - - foreach($firstItems as $k => $v) { - $this->firstItems[] = array($k,$v); - $this->excludedItems[] = $k; + $this->endItemIdx = null; + + if($firstItems) { + foreach($firstItems as $k => $v) { + $this->firstItems[] = array($k,$v); + $this->excludedItems[] = $k; + } + } + + if($lastItems) { + foreach($lastItems as $k => $v) { + $this->lastItems[] = array($k, $v); + $this->excludedItems[] = $k; + } } } - - // Iterator functions + /** + * Rewind the Iterator to the first element. + * + * @return mixed + */ public function rewind() { $this->firstItemIdx = 0; + $this->endItemIdx = null; + $rewoundItem = $this->items->rewind(); if(isset($this->firstItems[$this->firstItemIdx])) { return $this->firstItems[$this->firstItemIdx][1]; } else { - if($rewoundItem) return ($rewoundItem->hasMethod($this->titleField)) - ? $rewoundItem->{$this->titleField}() - : $rewoundItem->{$this->titleField}; + if($rewoundItem) { + if($rewoundItem->hasMethod($this->titleField)) { + return $rewoundItem->{$this->titleField}(); + } + + return $rewoundItem->{$this->titleField}; + } else if(!$this->items->valid() && $this->lastItems) { + $this->endItemIdx = 0; + + return $this->lastItems[0][1]; + } } - } + /** + * Return the current element. + * + * @return mixed + */ public function current() { - if(isset($this->firstItems[$this->firstItemIdx])) { + if(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) { + return $this->lastItems[$this->endItemIdx][1]; + } else if(isset($this->firstItems[$this->firstItemIdx])) { return $this->firstItems[$this->firstItemIdx][1]; } else { - return ($this->items->current()->hasMethod($this->titleField)) - ? $this->items->current()->{$this->titleField}() - : $this->items->current()->{$this->titleField}; + if($this->items->current()->hasMethod($this->titleField)) { + return $this->items->current()->{$this->titleField}(); + } + + return $this->items->current()->{$this->titleField}; } } + /** + * Return the key of the current element. + * + * @return string + */ public function key() { - if(isset($this->firstItems[$this->firstItemIdx])) { + if(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) { + return $this->lastItems[$this->endItemIdx][0]; + } else if(isset($this->firstItems[$this->firstItemIdx])) { return $this->firstItems[$this->firstItemIdx][0]; - } else { return $this->items->current()->{$this->keyField}; } } + /** + * Move forward to next element. + * + * @return mixed + */ public function next() { $this->firstItemIdx++; + if(isset($this->firstItems[$this->firstItemIdx])) { return $this->firstItems[$this->firstItemIdx][1]; - } else { - if(!isset($this->firstItems[$this->firstItemIdx-1])) $this->items->next(); + if(!isset($this->firstItems[$this->firstItemIdx-1])) { + $this->items->next(); + } if($this->excludedItems) { while(($c = $this->items->current()) && in_array($c->{$this->keyField}, $this->excludedItems, true)) { @@ -207,9 +394,34 @@ class SS_Map_Iterator implements Iterator { } } } + + if(!$this->items->valid()) { + // iterator has passed the preface items, off the end of the items + // list. Track through the end items to go through to the next + if($this->endItemIdx === null) { + $this->endItemIdx = -1; + } + + $this->endItemIdx++; + + if(isset($this->lastItems[$this->endItemIdx])) { + return $this->lastItems[$this->endItemIdx]; + } + + return false; + } } + /** + * Checks if current position is valid. + * + * @return boolean + */ public function valid() { - return $this->items->valid(); + return ( + (isset($this->firstItems[$this->firstItemIdx])) || + (($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) || + $this->items->valid() + ); } } diff --git a/tests/model/MapTest.php b/tests/model/MapTest.php index b5b50bbe1..e58bcfa58 100755 --- a/tests/model/MapTest.php +++ b/tests/model/MapTest.php @@ -1,6 +1,11 @@ sort('Name'); + $map = new SS_Map($list, 'Name', 'Comment'); + + $this->assertEquals(array( + 'This is a team comment by Bob', + 'This is a team comment by Joe', + 'Phil is a unique guy, and comments on team2' + ), $map->values()); + + + $map->push('Push', 'Item'); + + $this->assertEquals(array( + 'This is a team comment by Bob', + 'This is a team comment by Joe', + 'Phil is a unique guy, and comments on team2', + 'Item' + ), $map->values()); + + $map = new SS_Map(new ArrayList()); + $map->push('Push', 'Pushed value'); + + $this->assertEquals(array( + 'Pushed value' + ), $map->values()); + + $map = new SS_Map(new ArrayList()); + $map->unshift('Unshift', 'Unshift item'); + + $this->assertEquals(array( + 'Unshift item' + ), $map->values()); + } + + public function testArrayAccess() { $list = DataObjectTest_TeamComment::get(); $map = new SS_Map($list, 'Name', 'Comment'); @@ -65,6 +107,39 @@ class SS_MapTest extends SapphireTest { 'Joe', 'Phil' ), $map->keys()); + + $map->unshift('Unshift', 'Item'); + + $this->assertEquals(array( + 'Unshift', + 'Bob', + 'Joe', + 'Phil' + ), $map->keys()); + + $map->push('Push', 'Item'); + + $this->assertEquals(array( + 'Unshift', + 'Bob', + 'Joe', + 'Phil', + 'Push' + ), $map->keys()); + + $map = new SS_Map(new ArrayList()); + $map->push('Push', 'Item'); + + $this->assertEquals(array( + 'Push' + ), $map->keys()); + + $map = new SS_Map(new ArrayList()); + $map->unshift('Unshift', 'Item'); + + $this->assertEquals(array( + 'Unshift' + ), $map->keys()); } public function testMethodAsValueField() { @@ -80,16 +155,6 @@ class SS_MapTest extends SapphireTest { ), $map->values()); } - public function testValues() { - $list = DataObjectTest_TeamComment::get()->sort('Name'); - $map = new SS_Map($list, 'Name', 'Comment'); - $this->assertEquals(array( - 'This is a team comment by Bob', - 'This is a team comment by Joe', - 'Phil is a unique guy, and comments on team2' - ), $map->values()); - } - public function testUnshift() { $list = DataObjectTest_TeamComment::get(); $map = new SS_Map($list, 'Name', 'Comment'); @@ -137,7 +202,103 @@ class SS_MapTest extends SapphireTest { "Bob" => "Replaced", 0 => "(Select)", -1 => "(All)"), $map->toArray()); + } + public function testPush() { + $list = DataObjectTest_TeamComment::get(); + $map = new SS_Map($list, 'Name', 'Comment'); + + $map->push(1, '(All)'); + + $this->assertEquals(array( + "Joe" => "This is a team comment by Joe", + "Bob" => "This is a team comment by Bob", + "Phil" => "Phil is a unique guy, and comments on team2", + 1 => "(All)" + ), $map->toArray()); } + public function testCount() { + $list = DataObjectTest_TeamComment::get(); + $map = new SS_Map($list, 'Name', 'Comment'); + + $this->assertEquals(3, $map->count()); + + // pushing a new item should update the count + $map->push(1, 'Item pushed'); + $this->assertEquals(4, $map->count()); + + $map->unshift(2, 'Item shifted'); + $this->assertEquals(5, $map->count()); + + $map = new SS_Map(new ArrayList()); + $map->unshift('1', 'shifted'); + + $this->assertEquals(1, $map->count()); + + unset($map[1]); + $this->assertEquals(0, $map->count()); + } + + public function testIterationWithUnshift() { + $list = DataObjectTest_TeamComment::get()->sort('ID'); + $map = new SS_Map($list, 'Name', 'Comment'); + $map->unshift(1, 'Unshifted'); + + $text = ""; + + foreach($map as $k => $v) { + $text .= "$k: $v\n"; + } + + $this->assertEquals("1: Unshifted\n" + . "Joe: This is a team comment by Joe\n" + . "Bob: This is a team comment by Bob\n" + . "Phil: Phil is a unique guy, and comments on team2\n", $text + ); + } + + public function testIterationWithPush() { + $list = DataObjectTest_TeamComment::get()->sort('ID'); + $map = new SS_Map($list, 'Name', 'Comment'); + $map->push(1, 'Pushed'); + + $text = ""; + + foreach($map as $k => $v) { + $text .= "$k: $v\n"; + } + + $this->assertEquals("Joe: This is a team comment by Joe\n" + . "Bob: This is a team comment by Bob\n" + . "Phil: Phil is a unique guy, and comments on team2\n" + . "1: Pushed\n", $text + ); + } + + public function testIterationWithEmptyListUnshifted() { + $map = new SS_Map(new ArrayList()); + $map->unshift('1', 'unshifted'); + + $text = ""; + + foreach($map as $k => $v) { + $text .= "$k: $v\n"; + } + + $this->assertEquals("1: unshifted\n", $text); + } + + public function testIterationWithEmptyListPushed() { + $map = new SS_Map(new ArrayList()); + $map->push('1', 'pushed'); + + $text = ""; + + foreach($map as $k => $v) { + $text .= "$k: $v\n"; + } + + $this->assertEquals("1: pushed\n", $text); + } }