* @link http://www.pradosoft.com/
* @copyright Copyright © 2005-2014 PradoSoft
* @license http://www.pradosoft.com/license/
* @package Prado\Collections
*/
namespace Prado\Collections;
use Prado\TPropertyValue;
use Prado\Exceptions\TInvalidOperationException;
use Prado\Exceptions\TInvalidDataTypeException;
/**
* TPriorityMap class
*
* TPriorityMap implements a collection that takes key-value pairs with
* a priority to allow key-value pairs to be ordered. This ordering is
* important when flattening the map. When flattening the map, if some
* key-value pairs are required to be before or after others, use this
* class to keep order to your map.
*
* You can access, add or remove an item with a key by using
* {@link itemAt}, {@link add}, and {@link remove}. These functions
* can optionally take a priority parameter to allow access to specific
* priorities. TPriorityMap is functionally backward compatible
* with {@link TMap}.
*
* To get the number of the items in the map, use {@link getCount}.
* TPriorityMap can also be used like a regular array as follows,
*
* $map[$key]=$value; // add a key-value pair
* unset($map[$key]); // remove the value with the specified key
* if(isset($map[$key])) // if the map contains the key
* foreach($map as $key=>$value) // traverse the items in the map
* $n=count($map); // returns the number of items in the map
*
* Using standard array access method like these will always use
* the default priority.
*
* An item that doesn't specify a priority will receive the default
* priority. The default priority is set during the instantiation
* of a new TPriorityMap. If no custom default priority is specified,
* the standard default priority of 10 is used.
*
* Priorities with significant digits below precision will be rounded.
*
* A priority may also be a numeric with decimals. This is set
* during the instantiation of a new TPriorityMap.
* The default is 8 decimal places for a priority. If a negative number
* is used, rounding occurs into the integer space rather than in
* the decimal space. See {@link round}.
*
* @author Brad Anderson
* @package Prado\Collections
* @since 3.2a
*/
class TPriorityMap extends TMap
{
/**
* @var array internal data storage
*/
private $_d=array();
/**
* @var boolean whether this list is read-only
*/
private $_r=false;
/**
* @var boolean indicates if the _d is currently ordered.
*/
private $_o=false;
/**
* @var array cached flattened internal data storage
*/
private $_fd=null;
/**
* @var integer number of items contain within the map
*/
private $_c=0;
/**
* @var numeric the default priority of items without specified priorities
*/
private $_dp=10;
/**
* @var integer the precision of the numeric priorities within this priority list.
*/
private $_p=8;
/**
* Constructor.
* Initializes the array with an array or an iterable object.
* @param map|array|Iterator|TPriorityMap the intial data. Default is null, meaning no initialization.
* @param boolean whether the list is read-only
* @param numeric the default priority of items without specified priorities.
* @param integer the precision of the numeric priorities
* @throws TInvalidDataTypeException If data is not null and neither an array nor an iterator.
*/
public function __construct($data=null,$readOnly=false,$defaultPriority=10,$precision=8)
{
if($data!==null)
$this->copyFrom($data);
$this->setReadOnly($readOnly);
$this->setPrecision($precision);
$this->setDefaultPriority($defaultPriority);
}
/**
* @return boolean whether this map is read-only or not. Defaults to false.
*/
public function getReadOnly()
{
return $this->_r;
}
/**
* @param boolean whether this list is read-only or not
*/
protected function setReadOnly($value)
{
$this->_r=TPropertyValue::ensureBoolean($value);
}
/**
* @return numeric gets the default priority of inserted items without a specified priority
*/
public function getDefaultPriority()
{
return $this->_dp;
}
/**
* This must be called internally or when instantiated.
* @param numeric sets the default priority of inserted items without a specified priority
*/
protected function setDefaultPriority($value)
{
$this->_dp = (string)round(TPropertyValue::ensureFloat($value), $this->_p);
}
/**
* @return integer The precision of numeric priorities, defaults to 8
*/
public function getPrecision()
{
return $this->_p;
}
/**
* This must be called internally or when instantiated.
* @param integer The precision of numeric priorities.
*/
protected function setPrecision($value)
{
$this->_p=TPropertyValue::ensureInteger($value);
}
/**
* Returns an iterator for traversing the items in the map.
* This method is required by the interface \IteratorAggregate.
* @return Iterator an iterator for traversing the items in the map.
*/
public function getIterator()
{
return new \ArrayIterator($this->flattenPriorities());
}
/**
* Orders the priority list internally.
*/
protected function sortPriorities() {
if(!$this->_o) {
ksort($this->_d, SORT_NUMERIC);
$this->_o=true;
}
}
/**
* This flattens the priority map into a flat array [0,...,n-1]
* @return array array of items in the list in priority and index order
*/
protected function flattenPriorities() {
if(is_array($this->_fd))
return $this->_fd;
$this->sortPriorities();
$this->_fd = array();
foreach($this->_d as $priority => $itemsatpriority)
$this->_fd = array_merge($this->_fd, $itemsatpriority);
return $this->_fd;
}
/**
* Returns the number of items in the map.
* This method is required by \Countable interface.
* @return integer number of items in the map.
*/
public function count()
{
return $this->getCount();
}
/**
* @return integer the number of items in the map
*/
public function getCount()
{
return $this->_c;
}
/**
* Gets the number of items at a priority within the map.
* @param numeric optional priority at which to count items. if no parameter,
* it will be set to the default {@link getDefaultPriority}
* @return integer the number of items in the map at the specified priority
*/
public function getPriorityCount($priority=null)
{
if($priority===null)
$priority=$this->getDefaultPriority();
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
if(!isset($this->_d[$priority])||!is_array($this->_d[$priority]))
return false;
return count($this->_d[$priority]);
}
/**
* This returns a list of the priorities within this map, ordered lowest to highest.
* @return array the array of priority numerics in decreasing priority order
*/
public function getPriorities()
{
$this->sortPriorities();
return array_keys($this->_d);
}
/**
* Returns the keys within the map ordered through the priority of each key-value pair
* @return array the key list
*/
public function getKeys()
{
return array_keys($this->flattenPriorities());
}
/**
* Returns the item with the specified key. If a priority is specified, only items
* within that specific priority will be selected
* @param mixed the key
* @param mixed the priority. null is the default priority, false is any priority,
* and numeric is a specific priority. default: false, any priority.
* @return mixed the element at the offset, null if no element is found at the offset
*/
public function itemAt($key,$priority=false)
{
if($priority===false){
$map=$this->flattenPriorities();
return isset($map[$key])?$map[$key]:null;
} else {
if($priority===null)
$priority=$this->getDefaultPriority();
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
return (isset($this->_d[$priority])&&isset($this->_d[$priority][$key]))?$this->_d[$priority][$key]:null;
}
}
/**
* This changes an item's priority. Specify the item and the new priority.
* This method is exactly the same as {@link offsetGet}.
* @param mixed the key
* @param numeric|null the priority. default: null, filled in with the default priority numeric.
* @return numeric old priority of the item
*/
public function setPriorityAt($key,$priority=null)
{
if($priority===null)
$priority=$this->getDefaultPriority();
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
$oldpriority=$this->priorityAt($key);
if($oldpriority!==false&&$oldpriority!=$priority) {
$value=$this->remove($key,$oldpriority);
$this->add($key,$value,$priority);
}
return $oldpriority;
}
/**
* Gets all the items at a specific priority.
* @param numeric priority of the items to get. Defaults to null, filled in with the default priority, if left blank.
* @return array all items at priority in index order, null if there are no items at that priority
*/
public function itemsAtPriority($priority=null)
{
if($priority===null)
$priority=$this->getDefaultPriority();
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
return isset($this->_d[$priority])?$this->_d[$priority]:null;
}
/**
* Returns the priority of a particular item within the map. This searches the map for the item.
* @param mixed item to look for within the map
* @return numeric priority of the item in the map
*/
public function priorityOf($item)
{
$this->sortPriorities();
foreach($this->_d as $priority=>$items)
if(($index=array_search($item,$items,true))!==false)
return $priority;
return false;
}
/**
* Retutrns the priority of an item at a particular flattened index.
* @param integer index of the item within the map
* @return numeric priority of the item in the map
*/
public function priorityAt($key)
{
$this->sortPriorities();
foreach($this->_d as $priority=>$items)
if(array_key_exists($key,$items))
return $priority;
return false;
}
/**
* Adds an item into the map. A third parameter may be used to set the priority
* of the item within the map. Priority is primarily used during when flattening
* the map into an array where order may be and important factor of the key-value
* pairs within the array.
* Note, if the specified key already exists, the old value will be overwritten.
* No duplicate keys are allowed regardless of priority.
* @param mixed key
* @param mixed value
* @param numeric|null priority, default: null, filled in with default priority
* @return numeric priority at which the pair was added
* @throws TInvalidOperationException if the map is read-only
*/
public function add($key,$value,$priority=null)
{
if($priority===null)
$priority=$this->getDefaultPriority();
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
if(!$this->_r)
{
foreach($this->_d as $innerpriority=>$items)
if(array_key_exists($key,$items))
{
unset($this->_d[$innerpriority][$key]);
$this->_c--;
if(count($this->_d[$innerpriority])===0)
unset($this->_d[$innerpriority]);
}
if(!isset($this->_d[$priority])) {
$this->_d[$priority]=array($key=>$value);
$this->_o=false;
}
else
$this->_d[$priority][$key]=$value;
$this->_c++;
$this->_fd=null;
}
else
throw new TInvalidOperationException('map_readonly',get_class($this));
return $priority;
}
/**
* Removes an item from the map by its key. If no priority, or false, is specified
* then priority is irrelevant. If null is used as a parameter for priority, then
* the priority will be the default priority. If a priority is specified, or
* the default priority is specified, only key-value pairs in that priority
* will be affected.
* @param mixed the key of the item to be removed
* @param numeric|false|null priority. False is any priority, null is the
* default priority, and numeric is a specific priority
* @return mixed the removed value, null if no such key exists.
* @throws TInvalidOperationException if the map is read-only
*/
public function remove($key,$priority=false)
{
if(!$this->_r)
{
if($priority===null)
$priority=$this->getDefaultPriority();
if($priority===false)
{
$this->sortPriorities();
foreach($this->_d as $priority=>$items)
if(array_key_exists($key,$items))
{
$value=$this->_d[$priority][$key];
unset($this->_d[$priority][$key]);
$this->_c--;
if(count($this->_d[$priority])===0)
{
unset($this->_d[$priority]);
$this->_o=false;
}
$this->_fd=null;
return $value;
}
return null;
}
else
{
$priority=(string)round(TPropertyValue::ensureFloat($priority),$this->_p);
if(isset($this->_d[$priority])&&(isset($this->_d[$priority][$key])||array_key_exists($key,$this->_d[$priority])))
{
$value=$this->_d[$priority][$key];
unset($this->_d[$priority][$key]);
$this->_c--;
if(count($this->_d[$priority])===0) {
unset($this->_d[$priority]);
$this->_o=false;
}
$this->_fd=null;
return $value;
}
else
return null;
}
}
else
throw new TInvalidOperationException('map_readonly',get_class($this));
}
/**
* Removes all items in the map. {@link remove} is called on all items.
*/
public function clear()
{
foreach($this->_d as $priority=>$items)
foreach(array_keys($items) as $key)
$this->remove($key);
}
/**
* @param mixed the key
* @return boolean whether the map contains an item with the specified key
*/
public function contains($key)
{
$map=$this->flattenPriorities();
return isset($map[$key])||array_key_exists($key,$map);
}
/**
* When the map is flattened into an array, the priorities are taken into
* account and elements of the map are ordered in the array according to
* their priority.
* @return array the list of items in array
*/
public function toArray()
{
return $this->flattenPriorities();
}
/**
* Combines the map elements which have a priority below the parameter value
* @param numeric the cut-off priority. All items of priority less than this are returned.
* @param boolean whether or not the input cut-off priority is inclusive. Default: false, not inclusive.
* @return array the array of priorities keys with values of arrays of items that are below a specified priority.
* The priorities are sorted so important priorities, lower numerics, are first.
*/
public function toArrayBelowPriority($priority,$inclusive=false)
{
$this->sortPriorities();
$items=array();
foreach($this->_d as $itemspriority=>$itemsatpriority)
{
if((!$inclusive&&$itemspriority>=$priority)||$itemspriority>$priority)
break;
$items=array_merge($items,$itemsatpriority);
}
return $items;
}
/**
* Combines the map elements which have a priority above the parameter value
* @param numeric the cut-off priority. All items of priority greater than this are returned.
* @param boolean whether or not the input cut-off priority is inclusive. Default: true, inclusive.
* @return array the array of priorities keys with values of arrays of items that are above a specified priority.
* The priorities are sorted so important priorities, lower numerics, are first.
*/
public function toArrayAbovePriority($priority,$inclusive=true)
{
$this->sortPriorities();
$items=array();
foreach($this->_d as $itemspriority=>$itemsatpriority)
{
if((!$inclusive&&$itemspriority<=$priority)||$itemspriority<$priority)
continue;
$items=array_merge($items,$itemsatpriority);
}
return $items;
}
/**
* Copies iterable data into the map.
* Note, existing data in the map will be cleared first.
* @param mixed the data to be copied from, must be an array, object implementing
* Traversable, or a TPriorityMap
* @throws TInvalidDataTypeException If data is neither an array nor an iterator.
*/
public function copyFrom($data)
{
if($data instanceof TPriorityMap)
{
if($this->getCount()>0)
$this->clear();
foreach($data->getPriorities() as $priority) {
foreach($data->itemsAtPriority($priority) as $key => $value) {
$this->add($key,$value,$priority);
}
}
}
else if(is_array($data)||$data instanceof \Traversable)
{
if($this->getCount()>0)
$this->clear();
foreach($data as $key=>$value)
$this->add($key,$value);
}
else if($data!==null)
throw new TInvalidDataTypeException('map_data_not_iterable');
}
/**
* Merges iterable data into the map.
* Existing data in the map will be kept and overwritten if the keys are the same.
* @param mixed the data to be merged with, must be an array, object implementing
* Traversable, or a TPriorityMap
* @throws TInvalidDataTypeException If data is neither an array nor an iterator.
*/
public function mergeWith($data)
{
if($data instanceof TPriorityMap)
{
foreach($data->getPriorities() as $priority)
{
foreach($data->itemsAtPriority($priority) as $key => $value)
$this->add($key,$value,$priority);
}
}
else if(is_array($data)||$data instanceof \Traversable)
{
foreach($data as $key=>$value)
$this->add($key,$value);
}
else if($data!==null)
throw new TInvalidDataTypeException('map_data_not_iterable');
}
/**
* Returns whether there is an element at the specified offset.
* This method is required by the interface \ArrayAccess.
* @param mixed the offset to check on
* @return boolean
*/
public function offsetExists($offset)
{
return $this->contains($offset);
}
/**
* Returns the element at the specified offset.
* This method is required by the interface \ArrayAccess.
* @param integer the offset to retrieve element.
* @return mixed the element at the offset, null if no element is found at the offset
*/
public function offsetGet($offset)
{
return $this->itemAt($offset);
}
/**
* Sets the element at the specified offset.
* This method is required by the interface \ArrayAccess.
* @param integer the offset to set element
* @param mixed the element value
*/
public function offsetSet($offset,$item)
{
$this->add($offset,$item);
}
/**
* Unsets the element at the specified offset.
* This method is required by the interface \ArrayAccess.
* @param mixed the offset to unset element
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
}