API: Implement SS_Map::push() to append values.

FIX: SS_Map::count() not taking into account additional firstItems
This commit is contained in:
Will Rossiter 2013-11-16 13:05:30 +13:00
parent bd5fdf1f19
commit 0e89c4b6cd
2 changed files with 442 additions and 69 deletions

View File

@ -7,12 +7,26 @@
* @subpackage model * @subpackage model
*/ */
class SS_Map implements ArrayAccess, Countable, IteratorAggregate { class SS_Map implements ArrayAccess, Countable, IteratorAggregate {
protected $list, $keyField, $valueField; protected $list, $keyField, $valueField;
/**
* @see SS_Map::unshift()
*
* @var array $firstItems
*/
protected $firstItems = array(); protected $firstItems = array();
/**
* @see SS_Map::push()
*
* @var array $lastItems
*/
protected $lastItems = array();
/** /**
* Construct a new map around an SS_list. * Construct a new map around an SS_list.
*
* @param $list The list to build a map from * @param $list The list to build a map from
* @param $keyField The field to use as the key of each map entry * @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 * @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) { public function setKeyField($keyField) {
$this->keyField = $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) { public function setValueField($valueField) {
$this->valueField = $valueField; $this->valueField = $valueField;
} }
/** /**
* Return an array equivalent to this map * Return an array equivalent to this map.
*
* @return array
*/ */
public function toArray() { public function toArray() {
$array = array(); $array = array();
foreach($this as $k => $v) { foreach($this as $k => $v) {
$array[$k] = $v; $array[$k] = $v;
} }
return $array; return $array;
} }
/** /**
* Return all the keys of this map * Return all the keys of this map.
*
* @return array
*/ */
public function keys() { public function keys() {
$output = array(); return array_keys($this->toArray());
foreach($this as $k => $v) {
$output[] = $k;
}
return $output;
} }
/** /**
* Return all the values of this map * Return all the values of this map.
*
* @return array
*/ */
public function values() { public function values() {
$output = array(); return array_values($this->toArray());
foreach($this as $k => $v) {
$output[] = $v;
}
return $output;
} }
/** /**
* 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) { public function unshift($key, $value) {
$oldItems = $this->firstItems; $oldItems = $this->firstItems;
$this->firstItems = array($key => $value); $this->firstItems = array(
if($oldItems) $this->firstItems = $this->firstItems + $oldItems; $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; return $this;
} }
// ArrayAccess // ArrayAccess
/**
* @var string $key
*
* @return boolean
*/
public function offsetExists($key) { 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); $record = $this->list->find($this->keyField, $key);
return $record != null; return $record != null;
} }
/**
* @var string $key
*
* @return mixed
*/
public function offsetGet($key) { 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); $record = $this->list->find($this->keyField, $key);
if($record) { if($record) {
$col = $this->valueField; $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) { public function offsetUnset($key) {
if(isset($this->firstItems[$key])) { if(isset($this->firstItems[$key])) {
unset($this->firstItems[$key]); unset($this->firstItems[$key]);
return; 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() { 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() { 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 * Builds a map iterator around an Iterator. Called by SS_Map
*
* @package framework * @package framework
* @subpackage model * @subpackage model
*/ */
class SS_Map_Iterator implements Iterator { class SS_Map_Iterator implements Iterator {
protected $items; protected $items;
protected $keyField, $titleField; protected $keyField, $titleField;
protected $firstItemIdx = 0; protected $firstItemIdx = 0;
protected $endItemIdx;
protected $firstItems = array(); protected $firstItems = array();
protected $lastItems = array();
protected $excludedItems = array(); protected $excludedItems = array();
/** /**
* @param $items The iterator to build this map from * @param Iterator $items The iterator to build this map from
* @param $keyField The field to use for the keys * @param string $keyField The field to use for the keys
* @param $titleField The field to use for the values * @param string $titleField The field to use for the values
* @param $fistItems An optional map of items to show first * @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->items = $items;
$this->keyField = $keyField; $this->keyField = $keyField;
$this->titleField = $titleField; $this->titleField = $titleField;
$this->endItemIdx = null;
foreach($firstItems as $k => $v) {
$this->firstItems[] = array($k,$v); if($firstItems) {
$this->excludedItems[] = $k; 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() { public function rewind() {
$this->firstItemIdx = 0; $this->firstItemIdx = 0;
$this->endItemIdx = null;
$rewoundItem = $this->items->rewind(); $rewoundItem = $this->items->rewind();
if(isset($this->firstItems[$this->firstItemIdx])) { if(isset($this->firstItems[$this->firstItemIdx])) {
return $this->firstItems[$this->firstItemIdx][1]; return $this->firstItems[$this->firstItemIdx][1];
} else { } else {
if($rewoundItem) return ($rewoundItem->hasMethod($this->titleField)) if($rewoundItem) {
? $rewoundItem->{$this->titleField}() if($rewoundItem->hasMethod($this->titleField)) {
: $rewoundItem->{$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() { 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]; return $this->firstItems[$this->firstItemIdx][1];
} else { } else {
return ($this->items->current()->hasMethod($this->titleField)) if($this->items->current()->hasMethod($this->titleField)) {
? $this->items->current()->{$this->titleField}() return $this->items->current()->{$this->titleField}();
: $this->items->current()->{$this->titleField}; }
return $this->items->current()->{$this->titleField};
} }
} }
/**
* Return the key of the current element.
*
* @return string
*/
public function key() { 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]; return $this->firstItems[$this->firstItemIdx][0];
} else { } else {
return $this->items->current()->{$this->keyField}; return $this->items->current()->{$this->keyField};
} }
} }
/**
* Move forward to next element.
*
* @return mixed
*/
public function next() { public function next() {
$this->firstItemIdx++; $this->firstItemIdx++;
if(isset($this->firstItems[$this->firstItemIdx])) { if(isset($this->firstItems[$this->firstItemIdx])) {
return $this->firstItems[$this->firstItemIdx][1]; return $this->firstItems[$this->firstItemIdx][1];
} else { } else {
if(!isset($this->firstItems[$this->firstItemIdx-1])) $this->items->next(); if(!isset($this->firstItems[$this->firstItemIdx-1])) {
$this->items->next();
}
if($this->excludedItems) { if($this->excludedItems) {
while(($c = $this->items->current()) && in_array($c->{$this->keyField}, $this->excludedItems, true)) { 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() { public function valid() {
return $this->items->valid(); return (
(isset($this->firstItems[$this->firstItemIdx])) ||
(($this->endItemIdx !== null) && isset($this->lastItems[$this->endItemIdx])) ||
$this->items->valid()
);
} }
} }

View File

@ -1,6 +1,11 @@
<?php <?php
/**
* @package framework
* @subpackage tests
*/
class SS_MapTest extends SapphireTest { class SS_MapTest extends SapphireTest {
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
@ -16,6 +21,43 @@ class SS_MapTest extends SapphireTest {
'DataObjectTest_TeamComment' 'DataObjectTest_TeamComment'
); );
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());
$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() { public function testArrayAccess() {
$list = DataObjectTest_TeamComment::get(); $list = DataObjectTest_TeamComment::get();
$map = new SS_Map($list, 'Name', 'Comment'); $map = new SS_Map($list, 'Name', 'Comment');
@ -65,6 +107,39 @@ class SS_MapTest extends SapphireTest {
'Joe', 'Joe',
'Phil' 'Phil'
), $map->keys()); ), $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() { public function testMethodAsValueField() {
@ -80,16 +155,6 @@ class SS_MapTest extends SapphireTest {
), $map->values()); ), $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() { public function testUnshift() {
$list = DataObjectTest_TeamComment::get(); $list = DataObjectTest_TeamComment::get();
$map = new SS_Map($list, 'Name', 'Comment'); $map = new SS_Map($list, 'Name', 'Comment');
@ -137,7 +202,103 @@ class SS_MapTest extends SapphireTest {
"Bob" => "Replaced", "Bob" => "Replaced",
0 => "(Select)", 0 => "(Select)",
-1 => "(All)"), $map->toArray()); -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);
}
} }