Skip to content

Instantly share code, notes, and snippets.

@Jeff-Russ
Last active March 1, 2017 18:17
Show Gist options
  • Save Jeff-Russ/b56bb246fde14f013cc5233c20bcd6ec to your computer and use it in GitHub Desktop.
Save Jeff-Russ/b56bb246fde14f013cc5233c20bcd6ec to your computer and use it in GitHub Desktop.
PHP: Sequential Array Class

Sequence and TSequence

TSequence is an array trait with numerically sortable keys. Keys can be negative or positive integers or floats. It does not matter whether they are actually numeric types or string and if they are strings they are allowed to contain any character so long as they start with something numeric. The non-numeric ending of the string key, if present, is called the 'tag'.

You can optionally protect to an already assigned element by automatically incrementing the numeric portion of the key you provided until a free slot is found. (see offsetSet method)

Whenever you fetch the array as a whole with the getArrayCopy or getArrayRef methods, the array is sorted by (the numeric prefix portion of the) key.

Properties

  • array $seq object's array data. When setting, same as calling seq()
    • aliases: $data, $array
  • bool $auto_push determines whether to use $key for [] 'null keys'
    • aliases: $push, $autoPush
  • mixed $incr the value added to [$key] to avoid overwriting or not if falsy
    • aliases: $auto_incr, $autoIncr
  • mixed $key manually set key used with auto_push for temp use or, if null/false, sets $auto_push boolean
    • aliases: $push_key, $pushKey
  • mixed $depth default depth of search with search()
    • aliases: $search_depth, $searchDepth
  • mixed $search_mode default mode of search with search()

TSequence's Key Sanitizing

Numeric key will have any trailing zeros stripped and anything equaling an even number becomes an integer. Therefore you cannot have both '1' and '1.0'. Floats are converted to strings to prevent php's behavior of converting them to ints. Any key that does not have a numeric prefix or is not numeric will result in error.

[] - offsetSet() & offsetGet()

void offsetSet($key, $val) uses TSequence's Key Sanitizing. It is what is called when a value is assigned like $seq[$key] = $val;

When the key being assigned to already exists, a number of things could occur. If the auto_incr aka incr property is off (falsy) the value is reset. If it is a non-zero number or true (this sets it to the default 0.01) the number / numeric prefix, will be incremented until a free slot is found.

When assigned with [] or [null], two things could happen. If the auto_push property is true, The key pointed to by the $key property will be tried. This property is the previously accessed / set key or whatever you explicitly set it to with the ->key property. This key might or might not exist and if it does, the rules of auto_incr apply.

mixed offsetGet($key) also uses TSequence's Key Sanitizing. It is what is called when a value requested like $val = $seq[$key]; If the element is not set, no error occurs and null is returned.

isset($seq[$key])

bool offsetExists($key) applies TSequence's Key Sanitizing to provided key and returns true if the element exists, whether set to null or not. It is what is called when $bool = isset($seq[$key]); is typed.

unset($seq[$key])

void offsetUnset($key) is called when unset() is applied to the object at a location specified within square brackets. It applies TSequence's Key Sanitizing to provided key before performing unset.

$seq->set()

$this set(...) is used to 'forcefully' set an element, meaning it ignores settings and essentially behaves as assignment with [] on a normal array would, only when a key is not specified, it will be the $key property value, which could have been determined from accessing or setting.

  • 1 argument forcefully sets the previous key to the $arg1 value.
  • 2 arguments forcefully sets the the element at $arg1 to the value $arg2
  • 3+ arguments forcefully sets the the element at $arg1 to the remaining arguments, shifted to start at index 0 rather than 1.

Arguments:

  • mixed $arg1 can be an array key or a value being set to previous key
  • mixed $arg2 is the one and only value being assigned if the last arg
  • mixed $arg3 if supplied, is index 1 ($arg2 is index 0)
  • mixed $arg2 This and any future argument would be indexes 2 and beyond

$seq->seq()

$this seq($a) sets the full content of the array object. It can be used to clobber what's there or set it for the first time, as if from the array constructor. Pass it something empty or falsy to simply clear it. seq() is capable of accepting any number of argument and if more than one, The entire argument array is the array being assigned (with int keys). Arguments:

  • mixed $a an array to set to or the first value of the first element

$seq->merge()

$this merge($a) behaves similar to array_merge() only each element being merged in is added as if assigned with the subscript and offsetSet, That means if you have auto_incr set you won't be overwriting anything, otherwise you would clobber any already occupied keys.

There is normally only one argument, an array, but if it is not an array, it is pushed to the array as if assigned the []= ( possibly using $uto_incr and/or $key ) and if you have more than one argument they are pushed one by one in this same manner. Arguments:

  • mixed $a an array to merge in, value to be pushed or the first of many.

$seq->ksort()

$this ksort() sorts the array by key.

$seq->reIndex()

$this reIndex($from, $incr, $to) ksorts the sequence and changes the numeric component of each key to create an even spread set by an increment (argument 2), which defaults to 1.

The first and third arguments set the first and last index, respectively. They default to 0 and 'auto', respectively. 'auto' simply means that the last index will be whatever it needs to be if starting at 0 and incrementing by adding 1 for each key.

The first and third args can be set to 'auto' or 'retain', which means they will retain their current index/numeric value.

The second argument sets the increment value and can be a numeric or 'auto', but note that only one argument can be 'auto' at a given time.

All indexes are integers unless the fourth argument is set to true or a non-integer numeric is found in the first three arguments.

Valid arguments configurations:

  • NUMBER, 'auto', 'retain' or NUMBER
  • NUMBER, NUMBER, 'auto'
  • 'retain', 'auto', 'retain' or NUMBER
  • 'retain', NUMBER, 'auto'
  • 'auto', NUMBER, 'retain' or NUMBER

Arguments:

  • mixed $from can be a numeric, 'retain' or 'auto'
  • mixed $incr can be a numeric or 'auto'
  • mixed $to can be a numeric, 'retain' or 'auto'
  • bool $allow_flt sets whether indexes can be floating point

$seq->search()

mixed search($val, $mode=default, $strict=true) Searches for the key(s) for a given value, provided as the first argument, in the sequence array. Does various types of searches depending on the value of the second argument, $mode:

  • 'key' - One-dimensional search, returns key
  • 'num' - One-dimensional search, returns key's numeric prefix
  • 'int' - One-dimensional search, returns key's numeric prefix convert to int.
  • 'tag' - One-dimensional search, returns key's non-numeric portion, including space

There are also a few recursive search mode, where a single result of each is an array representing the 'path' of keys approaching the value. The first element is the top key, next is the sub-array's key, etc. If no match if found, false is returned. Note that 'default depth' below refers to the value of the object's $depth property. If depth is 1, the match would an array of one element, the top level key or 2 element, the top level key and the first 'deep' key.

  • 'deep' - recursive search with default depth.
  • '8deep' - same as above but with custom depth of 8 - array would be 1-9 keys
  • '8', 8 - same as above since 'deep' is the default recursive mode.
  • 'all' - returns all (as array of arrays), not just the first match, using default depth.
  • '3 all' - same as above but with custom depth of 3.

If $mode is anything else, a one-dimensional search is performed and the matched key is chopped up and return as an array with 'num','idx','space','tag'

Arguments:

  • mixed $val is the value who's key we are searching for.
  • mixed $mode see above
  • bool $strict if true, run strict search
<?php
/**
* TSequence is an array trait with numerically sortable keys. Keys can be
* negative or positive integers or floats. It does not matter whether they are
* actually numeric types or string and if they are strings they are allowed to
* contain any character so long as they start with something numeric.
* The non-numeric ending of the string key, if present, is called the 'tag'.
*
* You can optionally protect to an already assigned element by automatically
* incrementing the numeric portion of the key you provided until a free slot is
* found. (see offsetSet method)
*
* Note that only
*
* Whenever you fetch the array as a whole with the getArrayCopy or getArrayRef
* methods, the array is sorted by (the numeric prefix portion of the) key.
*
* @property array $seq object's array data. When setting, same as calling seq().
* aliases: $data, $array
* @property bool $auto_push determines whether to use $key for [] 'null keys'
* aliases: $push, $autoPush
* @property mixed $incr the value added to [$key] to avoid overwriting or not if falsy
* aliases: $auto_incr, $autoIncr
* @property mixed $key manually set key used with auto_push for temp use
* or, if null/false, sets $auto_push boolean
* aliases: $push_key, $pushKey
* @property mixed $depth default depth of search with search()
* aliases: $search_depth, $searchDepth
* @property mixed $search_mode default mode of search with search()
**/
trait TSequence #implements Serializable, IteratorAggregate, ArrayAccess, Countable
{
protected $seq = array();
protected $incr = false;
protected $auto_null = false;
protected $key = 0;
protected $depth = 1;
protected $search_mode = 'keys';
static protected $hasnum = '/^[-+]?[0-9].*$/';
static protected $ksplit = '/^([-+]?[\d]+\.?[\d]*)(.*)$/';
static protected $kparse = '/^([-+]?[\d]+\.?[\d]*)(\s*)(.*)$/';
function __get($prop) { return $this->_validGetSet($prop, 'get'); }
protected function _validGetSet($name, $mode=true) {
$lu = array(
'auto_null'=>'auto_null', 'push'=>'auto_null', 'auto'=>'auto_null', #
'incr'=>'incr', 'auto_incr'=>'incr',#
'push_key'=>'key', 'key'=>'key', #
'seq'=>'seq', 'data'=>'seq', 'array'=>'seq',#
'depth'=>'depth', 'search_depth'=>'depth',
'search_mode'=>'search_mode',
);
if (isset($lu[$name])) return $mode==='get' ? $this->{$lu[$name]}
: array('is'=>'prop', 'name'=>$lu[$name]);
if ( ($snake=strtolower(preg_replace('/(?<!^)[A-Z]/','_$0',$name)))
&& isset($lu[$snake]) ) return $mode==='get' ? $this->{$lu[$snake]}
: array('is'=>'prop', 'name'=>$lu[$snake]);
if (preg_match('/^(?:push_)?(?:key_)?(.+)$/',$snake,$n)&&isset($n[1])
&& in_array($n[1],array('num','idx','tag','space'),true)
) {
preg_match(self::$kparse,$this->key,$c); $c = array_pad($c,4,'');
$arr = array( 'is'=>'meta', 'name'=>$n[1], 'key'=>$this->key,
'num'=>$c[1],'idx'=>(int)$c[1],'space'=>$c[2],'tag'=>$c[3]
);
return $mode==='get' ? $arr[$n[1]] : $arr;
}
if (property_exists($this, $name)) return $mode==='get' ? $this->$name
: array('is'=>'other', 'name'=>$name);
if (property_exists($this, $snake)) return $mode==='get' ? $this->$snake
: array('is'=>'other', 'name'=>$snake);
if ($mode==='get') $mode = E_USER_ERROR;
$msg = get_called_class()."->$name invalid property or meta-property name";
if (!$mode) $ret = array('is'=>null, 'name'=>$msg);
trigger_error($msg, (int)$mode); return array();
}
function __set($prop, $val) {
extract($this->_validGetSet($prop, E_USER_ERROR));
if ($is==='prop') {
if ($name==='seq') return $this->seq($val);# fast shortcut for this
if ($name==='incr') {
if (!$val) $this->incr = false;
else $this->incr = is_numeric($val) ? $val : 0.01;
return $this->incr;
}
if ($name==='auto_null') return $this->auto_null = (bool)$val;
if ($name==='key') {
if ($val===null||$val===false) return $this->auto_null=(bool)$val;
$k = is_numeric($val) ? (string)($val+0) : "$val";
if (preg_match(self::$hasnum,$k)) return $this->key = "$k";
}
if ($name==='depth') return $this->depth = (int)$val;
if ($name==='search_mode') return $this->search_mode = $val;
} elseif ($is==='meta') {
if ($name==='tag') {
# kill space if tag is being deleted or if tag provided has space
if (!$tag || $tag[1]===' ') $space = '';
return $this->key = $num.$space.$tag;
}
if ($name==='idx' && is_int($val)) {
$arr = explode('.', $num, 2);
$frac = isset($arr[1]) ? ".{$arr[1]}" : '';
return $this->key = ((string)($val+0)).$frac.$space.$tag;
}
if ($name==='num' && is_numeric($val))
return $this->key = ((string)($val+0)).$space.$tag;
if ($name==='space' && ($has=ctype_space($val)) || !$val)
return $this->key = $num . ($has ? $val : '') . $tag;
}
return $this->kTypeErr($val);
}
/**
* @return the first key with current sorting (not after ksort).
*/
function resetKey() { reset($this->seq); return key($this->seq); }
/**
* @return the last key with current sorting (not after ksort).
*/
function endKey() { end($this->seq); return key($this->seq); }
/**
* @return the last key after hypothetical ksort
*/
function maxKey() { return max(array_keys($this->seq)); }
/**
* @return the first key after hypothetical ksort
*/
function minKey() { return min(array_keys($this->seq)); }
/**
* newKey() adds a new element to the sequence with assurance the key did not
* previously exist, whether set to null or not. A new key MUST be passed
* as the first argument and, if it exists or is invalid, false is returned.
*
* @param mixed $val value to set or first of many in array if there are more args
* @return mixed the key of the newly created element or false
*/
function newKey($key=null, $val=null)
{
$k = is_numeric($key) ? (string)($key+0) : "$key";# remove .0 trail
if (!preg_match(self::$hasnum,$k)) return false;
if (!array_key_exists($k, $this->seq)) {
$this->seq[$k] = $val; return $k;
}
if (func_num_args()>2) {# multi-arg value becomes array
$val=func_get_args(); unset($val[0]); $val=array_values($val);
}
do $num += $incr; while( array_key_exists("$num$end", $this->seq) );
$this->key="$num$end"; $this->seq[$k] = $val; return $k;
}
/**
* newIncr() adds a new element to the sequence with assurance the key did not
* previously exist, whether set to null or not. The new key will be an
* version of $this->key with it's numeric portion incremented.
*
* The first argument can be used to explicitly set a temporaray increment
* value or, if null, $this->incr will be used if truthy or 0.01 as default.
*
* @param mixed $incr increment and/or tag information for new key
* @param mixed $val value to set or first of many in array if there are more args
* @return mixed the key of the newly created element
*/
function newIncr($incr=null, $val=null)
{
if (!is_numeric($incr)) $incr = $this->incr ? $this->incr : 0.01;
if (func_num_args()>2) {# multi-arg value becomes array
$val=func_get_args(); unset($val[0]); $val=array_values($val);
}
$k = $this->key;
if (!array_key_exists($k, $this->seq)){ $this->seq[$k]=$val; return $k; }
// else we need to increment, but only the numeric prefix:
if ( preg_match(self::$ksplit, $k, $c) && isset($c[2])) {
$num = $c[1]; $end = $c[2];
} else { $num = $k; $end = ''; }
do $num += $this->incr; while( array_key_exists("$num$end", $this->seq) );
$this->key="$num$end"; $this->seq[$this->key] = $val; return $this->key;
}
/**
* newMax() adds a new element to the sequence with assurance the key did not
* previously exist, whether set to null or not. It assures the new key would
* be of maximum value, meaning after a (not actually preformed) ksort.
*
* The optional first argument can be used to set the increment value which
* otherwise would be $this->incr or 1 as a default. If the first argument is
* a non numeric string, it's parsed as if it's a key, only it does not need
* have a numeric prefix but if it does, that's the increment value and the
* portion after is what will be the tag of the new key.
*
* @param mixed $incr increment and/or tag information for new key
* @param mixed $val value to set or first of many in array if there are more args
* @return mixed the key of the newly created element
*/
function newMax($incr=null, $val=null)
{
if (func_num_args()>2) {# multi-arg value becomes array
$val=func_get_args(); unset($val[0]); $val=array_values($val);
}
if ($incr===null){ $incr=$this->incr? $this->incr : 1; $end = ''; }
elseif (is_numeric($incr)) { $incr=$incr; $end = ''; }
elseif (preg_match(self::$ksplit,$k,$c)){ $incr=$c[1]; $end=$c[2];}
else { $incr=$this->incr? $this->incr : 1; $end = ''; }
$k = $incr+(float)max(array_keys($this->seq)) . $end;
if (array_key_exists($k, $this->seq)) return false;
$this->key = $k; $this->seq[$k]; return $k;
}
/**
* newMin() adds a new element to the sequence with assurance the key did not
* previously exist, whether set to null or not. It is exactly the same as
* append only the new key is below the lowest key using the increment
* value (determined in the same way as newMax ) as a decrement.
*
* @param mixed $decr increment and/or tag information for new key
* @param mixed $val value to set or first of many in array if there are more args
* @return mixed the key of the newly created element
*/
function newMin($decr=null, $val=null)
{
if (func_num_args()>2) {# multi-arg value becomes array
$val=func_get_args(); unset($val[0]); $val=array_values($val);
}
if ($decr===null){ $decr=$this->incr? $this->incr : 1; $end = ''; }
elseif (is_numeric($decr)) { $decr=$decr; $end = ''; }
elseif (preg_match(self::$ksplit,$k,$c)){ $decr=$c[1]; $end=$c[2];}
else { $decr=$this->incr? $this->incr : 1; $end = ''; }
$k = (float)max(array_keys($this->seq))-$decr . $end;
if (array_key_exists($k, $this->seq)) return false;
$this->key = $k; $this->seq[$k]; return $k;
}
/**
* set() is used to 'forcefully' set an element, meaning it ignores settings
* and essentially behaves as assigment with [] on a normal array would, only
* when a key is not specified, it will be the $key property value, which
* could have been determined from accessing or setting.
*
* 1 argument forcefully sets the previous key to the $arg1 value.
* 2 arguments forcefully sets the the element at $arg1 to the value $arg2
* 3+ arguments forcefully sets the the element at $arg1 to the remaining
* arguments, shifted to start at index 0 rather than 1.
* @param mixed $arg1 can be an array key or a value being set to previous key
* @param mixed $val value to set or first of many in array if there are more args
* @return mixed returns $this or false if key is not valid.
*/
function set($arg1, $val=null) {
if (($argc=func_num_args())===1) {
$this->seq[$this->key] = $arg1; return $this->key;
}
if ($arg1===null) $k = $this->key;
else {
$k = is_numeric($arg1) ? (string)($arg1+0) : "$arg1";# remove .0 trail
if (!preg_match(self::$hasnum,$k)) return false;
}
if ($argc>2){$val=func_get_args(); unset($val[0]); $val=array_values($val);}
$this->seq[$k] = $val; return $this;
}
/**
* seq() sets the full content of the array object. It can be used
* to clobber what's there or set it for the first time, as if from the
* array constructor. Pass it something empty or falsy to simply clear it.
* seq() is capable of accepting any number of argument and if more than one,
* The entire argument array is the array being assigned (with int keys).
*
* @param mixed $a an array to set to or the first value of the first element
* @return object returns $this
*/
function seq($a) {
if (($argc=func_num_args())>1) {
$this->seq=func_get_args(); $this->key=$argc-1; return $this;
}
if (!$a) {$this->seq=array(); $this->key=0; return $this;}#clear all
$m = 'getArrayCopy'; // to de-clutter complex condition below
if ( is_array($a) || (method_exists($a,$m) && $a=$a->$m()||true) ) {
$bak = $this->seq; $this->seq=array(); // array need to be sanitized:
foreach ($a as $k=>$v) {
$k = is_numeric($k) ? (string)($k+0):"$k"; # remove .0 trail
if (!preg_match(self::$hasnum,$k)) return $this->kTypeErr($k,$bak);
$this->seq[$k] = $v;
}
$this->key = $k;
}
else { $this->seq = array($a); $this->key=0; }# set array to one el.
return $this;
}
/**
* offsetSet() uses TSequence's Numeric Key Formatting:
*
* If keys is numeric, trailing zeros are stripped and anything equaling an
* even number becomes an integer. Floats are converted to strings to
* prevents php's behavior of converting them to ints. Any key that does not
* have a numeric prefix or is not numeric will result in error.
*
* When the key being assigned to already exists, a number of things could
* occur. If the auto_incr aka incr property is off (falsy) the value is reset.
* If it is a non-zero number or true (this sets it to the default 0.01)
* the number / numeric prefix, will be incremented until a free slot is found.
*
* When assigned with [] or [null], two things could happen. If the auto_push
* property is true, The key pointed to by the $key property will be tried.
* This property is the previously accessed / set key or whatever you explicitly
* set it to with the ->key property. This key might or might not exist and
* if it does, the rules of auto_incr apply.
*
* @param mixed $k key of desired element to assign
* @param mixed $val value to assign
*/
function offsetSet($k, $val) {
if ($k===null) {
if ($this->auto_null) $k = $this->key;
else { $this->seq[] = $val;
end($this->seq); $this->key = key($this->seq); return $this;
}
} else {
$k = is_numeric($k) ? (string)($k+0) : "$k";# remove .0 trail
if (!preg_match(self::$hasnum,$k)) return $this->kTypeErr($k);
}
if (!$this->incr||!isset($this->seq[$k])){
$this->key=$k; $this->seq[$this->key]=$val; return $this;
}
// else we need to increment, but only the numeric prefix:
if ( preg_match(self::$ksplit, $k, $c) && isset($c[2])) {
$num = $c[1]; $end = $c[2];
} else { $num = $k; $end = ''; }
do $num += $this->incr; while( isset($this->seq["$num$end"]) );
$this->key="$num$end"; $this->seq[$this->key] = $val; return $this;
}
/**
* offsetGet also uses TSequence's Numeric Key Formatting
* If the element is not set, no error occurs and null is returned.
* @param mixed $k key of desired element to fetch
*/
function offsetGet($k) {
$k = is_numeric($k) ? (string)($k+0) : "$k"; // sanitize
if (!isset($this->seq[$k])) return null;
return $this->seq[ ($this->key=$k) ];
}
/**
* offsetExists applies TSequence's Numeric Key Formatting to provided key
* and returns true if the element exists and it not null (using isset());
* @param mixed $k key of desired element to fetch
* @return bool whether key is set to a non-null value.
*/
function offsetExists($k) {
return isset( $this->seq[ is_numeric($k)?(string)($k+0):"$k" ] );
}
/**
* exists applies TSequence's Numeric Key Formatting to provided key
* and returns true if the element exists, whether set to null or not.
* @param mixed $k key of desired element to fetch
* @return bool whether key exists, whether set to null or not.
*/
function exists($k) {
return array_key_exists(is_numeric($k)?(string)($k+0):"$k", $this->seq);
}
/**
* offsetUnset is called when unset is applied to the object at a location
* specifed within square brackets. It applies TSequence's Numeric Key
* Formatting to provided key before performing unset.
* @param mixed $k key of desired element to fetch
* @return object returns $this
*/
function offsetUnset($k) {unset($this->seq[is_numeric($k)?(string)($k+0):"$k"]);}
/**
* merge() behaves similar to array_merge() only each element being merged in
* is added as if assigned with the subscript and offsetSet, That means if
* you have auto_incr set you won't be overwritting anything, otherwise you
* would clobber any already occupied keys.
*
* There is normally only one argument, an array, but if it is not an array,
* it is pushed to the array as if assigned the []= ( possibly using $uto_incr
* and/or $key ) and if you have more than one argument they are pushed
* one by one in this same manner.
* @param mixed $a an array to merge in, value to be pushed or the first of many.
* @return object returns $this
*/
function merge($a=array()) {
if (!$this->seq) return $this->seq(func_get_args());
$m = 'getArrayCopy'; // to de-clutter complex elseif conditional below
if (($argc=func_num_args())===0) return $this;
if ($argc>1) $a=func_get_args();
elseif ( is_array($a) || (method_exists($a,$m) && $a=$a->$m() || true ) )
if (!$a) return $this;
else $a = array($a);
// non-empty array neeeds sanitizing.
foreach ($a as $k=>$v) { if (!$this->offsetSet($k, $v)) return false; }
return $this;# skip setting $this->key since offsetSet took care of it
}
/**
* ksort() sorts the array by key.
* @return object returns $this
*/
function ksort() { ksort($this->seq); return $this; }
/** @ignore */
function __toString() { ksort($this->seq); return var_export($this->seq, true); }
/** @ignore */
function getArrayCopy(){ ksort($this->seq); return $this->seq; }
/** @ignore */
function &getArrayRef(){ ksort($this->seq); return $this->seq; }
/** @ignore */
function unserialize($serialized) {
$this->seq = unserialize($serialized); ksort($this->seq);
}
/** @ignore */
function serialize() { ksort($this->seq); return serialize($this->seq); }
/** @ignore */
function getIterator() { ksort($this->seq); return new ArrayIterator($this->seq); }
/**
* reIndex ksorts the sequence and changes the numeric component of
* each key to create an even spread set by an increment (argument 2),
* which defaults to 1.
*
* The first and third arguments set the first and last index, respectively.
* They default to 0 and 'auto', respectively. 'auto' simply means that the
* last index will be whatever it needs to be if starting at 0 and incrementing
* by adding 1 for each key.
*
* The first and third args can be set to 'auto' or 'retain', which means
* they will retain their current index/numeric value.
*
* The second argument sets the increment value and can be a numeric or 'auto',
* but note that only one argument can be 'auto' at a given time.
*
* All indexes are integers unless the fourth argument is set to true or
* a non-integer numeric is found in the first three arguments.
*
* Valid arguments:
* #1 #2 NUMBER, 'auto', 'retain' or NUMBER
* #3 NUMBER, NUMBER, 'auto'
* #4 #5 'retain', 'auto', 'retain' or NUMBER
* #6 'retain', NUMBER, 'auto'
* #7 #8 'auto', NUMBER, 'retain' or NUMBER
* @param mixed $from can be a numeric, 'retain' or 'auto'
* @param mixed $incr can be a numeric or 'auto'
* @param mixed $to can be a numeric, 'retain' or 'auto'
* @param bool $allow_flt sets whether indexes can be floating point
* @return object returns $this
*/
function reIndex($from=0, $incr=1, $to='auto', $allow_flt=null) {
ksort($this->seq); reset($this->seq); $reset = key($this->seq);
if (($argc=func_num_args())!==0) # argument parsing
{
if ($allow_flt===null) {
$allow_flt = ( !is_int($from) && is_numeric($from)
|| $argc>1 && !is_int($incr) && is_numeric($incr)
|| $argc>2 && !is_int($to) && is_numeric($to) );
}
end($this->seq); $end = key($this->seq);
$c = count($this->seq); # we might need this
# PHP will warn for non-numeric so we can skip some is_numeric calls
if ($from===0 || is_numeric($from)){ # $incr is ignored/overwritten
if ($incr==='auto'){
if ($to==='retain') $to = $end; #1, else #2 w/numeric $to
$incr = (($to = $allow_flt ? $to : (int)$to) - $from) / $c;
}#else#3 w/numeric$incr
}
elseif ($from==='retain'){
$from = $allow_flt ? $reset : (int)$reset;
if ($incr==='auto'){
if ($to==='retain') $to=$end;#4, else#5 w/numeric $to
$incr = (($to = $allow_flt ? $to : (int)$to) - $from) / $c;
}#else #6 skipping is_numeric($incr)
}
elseif ($from==='auto'){
if ($to==='retain') $to = $end;#7 w/numeric$incr, else#8 w/numeric$to
$from = ($to = $allow_flt ? $to : (int)$to) - $incr * $c;
}
else return trigger_error("ReIndex invalid 1st arg",E_USER_ERROR)&&false;
if (!$allow_flt) $incr = (int)$incr;
}
/* run the action ...................................*/
$seq = $backup = $this->seq; $this->seq=array(); #sort & backup
$k = $reset; $i = $from;
$str = preg_match(self::$ksplit,$k,$c) && isset($c[2]) ? $c[2]:'';
$this->seq["$i".$str] = $reset_val = $seq[$k];# $reset_val is backup
unset($seq[$reset]); $i = $i + $incr;
foreach ($seq as $k=>$v){
if ($to!=='auto' && $k===$end) $i = $to;
$str = preg_match(self::$ksplit,$k,$c) && isset($c[2]) ? $c[2]:'';
if ( isset($this->seq[ ($new_k="$i".$str) ]) ) {
$this->seq = array($reset=>$reset_val)+$seq;
throw new Exception("[$k] would result in reIndex overflow");
}
$this->seq[$new_k] = $seq[$k];
$i = $i + $incr;
}
return $this;
}
/**
* Searches for the key(s) for a given value, provided as the first argument,
* in the sequence array. Does various types of searches depending on
* the value of the second argument, $mode:
*
* 'key' - One-dimensional search, returns key
* 'num' - One-dimensional search, returns key's numeric prefix
* 'int' - One-dimensional search, returns key's numeric prefix convert to int.
* 'tag' - One-dimensional search, returns key's non-numeric portion, including space
*
* There are also a few recursive search mode, where a single result of
* each is an array representing the 'path' of keys approaching the value.
* The first element is the top key, next is the subarray's key, etc. If no
* match if found, false is returned. Note that 'defualt depth' below refers
* to the value of the object's $depth property. If depth is 1, the match would
* an array of one element, the top level key or 2 element, the top level key
* and the first 'deep' key.
*
* 'deep' - recursive search with default depth.
* '8deep' - same as above but with custom depth of 8 - array would be 1-9 keys
* '8', 8 - same as above since 'deep' is the default recursive mode.
* 'all' - returns all (as array of arrays), not just the first match, using default depth.
* '3 all' - same as above but with custom depth of 3.
*
* If $mode is anything else, a one-dimensional search is performed and the
* matched key is chopped up and return as an array with 'num','idx','space','tag'
*
* @param mixed $val is the value who's key we are searching for.
* @param mixed $mode see above
* @param bool $strict if true, run strict search
* @return object returns $this
*/
function search($val, $mode=null, $strict=true) {
if ($mode===null) $mode = $this->search_mode;
if (strpos($mode, 'all')!==false) {
$depth = ($depth=(int)$mode) ? $depth : $this->depth;
$r = function ($val, $a, $d, $s, $l=0, &$path=[], &$keys=[], &$i=0) use (&$r) {
foreach($a as $k=>$v) {
$path[] = $k;
if (!$s && $v==$val || $v===$val) $keys[$i++] = $path;
elseif ($l<$d && (is_array($v) /*|| method_exists($v,'getIterator')*/))
$r($val, $v ,$d, $s, $l, $path, $keys, $i);
array_pop($path);
}
if ($l!==0) return $keys ? $keys : false;
return array_values( $keys );
};
return $r($val, $this->seq, $depth, $strict);
}
elseif (is_numeric($mode) || strpos($mode, 'deep')!==false) {
$depth = ($depth=(int)$mode) ? $depth : $this->depth;
$r = function ($val, $a, $d, $s, $l=0, $keys=array()) use (&$r) {
foreach($a as $k=>$v) {
if (!$s && $v==$val || $v===$val) { $keys[]=$k; return $keys; }
elseif ($l<$d && (is_array($v) /*|| method_exists($v,'getIterator')*/)) {
$keys[]=$k;
if ($keys=$r($val, $v ,$d, $s, $l+1, $keys)) return $keys;
}
}
return false;
};
return $r($val, $this->seq, $depth, $strict);
}
if (($k=array_search($val,$this->seq,$strict))===false) return false;
else $this->key = $k; // i guess
if ($mode==='key') return $k;
elseif($mode==='num') { preg_match(self::$kparse,$k,$c); return $c[1]; }
elseif($mode==='idx') { preg_match(self::$kparse,$k,$c); return (int)$c[1]; }
elseif($mode==='tag') {
preg_match(self::$kparse,$k,$c); return isset($c[3]) ? $c[3] : null;
} else {
preg_match(self::$kparse, $k, $c);
$c = array_pad($c, 4, null);
return array('num'=>$c[1],'idx'=>(int)$c[1],'space'=>$c[2],'tag'=>$c[3]);
}
}
/** @ignore */
function count() { return count( $this->seq ); }
/** @ignore */
function length() { return count( $this->seq ); }
/** @ignore */
function show() { var_export($this->seq); return $this;}
/** @ignore */
protected function kTypeErr($k, $arg2=E_USER_ERROR) {
if (is_array($arg2)) : $this->seq=$arg2; $flag=E_USER_WARNING;
elseif (is_integer($arg2)): $flag=$arg2; endif;
debug_print_backtrace();
return trigger_error(get_called_class()." keys must be or start with number"
. ($k ? ", received '$k'" :''), $flag) && false;
}
}
class Sequence implements Serializable, IteratorAggregate, ArrayAccess, Countable
{
use TSequence;
function __construct($a1=null, $a2=null)
{
if (($argc=func_num_args())===1):
call_user_func(array($this,'seq'), $a1);
elseif ($argc===2):
call_user_func(array($this,'seq'), $a1, $a2);
elseif ($argc>2):
call_user_func_array(array($this,'seq'), func_get_args());
endif;
}
}
$seq = new Sequence;
$seq->seq = array(
0=>array(1=>array(2=>'depth2') ),
'1.4' => 0,
'1depth1'=>array('depth1'),
'2arr1'=>array(
'arr2'=>array(
'arr3'=>array('3findme'),
),
),
'3findme1'=>array('findme'),
);
print_vals($seq->search('depth1')); # ['depth1', 0]
print_vals($seq->search('depth1', 0)); # [ ]
print_vals($seq->search('depth1', 0, false));# ['1.4'] beware! non-strict
print_vals($seq->search('depth2')); # [ ] default depth is 1
print_vals($seq->search('depth2', 2)); # [0,1,2] depth now 2
print_vals($seq->search('findme', 3)); # ['arr1','arr2','arr3',0]
// note that the value 'findme' occured twice but we only caught one
print_vals($seq->search('findme', '3 all')); # ['arr1','arr2','arr3',0]
/* a third argument containing 'all' and optionally starting with an int
* will return an array of recursive_search values:
[
['arr1', 'arr2', 'arr3', 0],
['findme1', 0],
]
*/
////////////////////////////////////////////////////////////////////////////////
// just to display demo:
function print_vals($arr) {
if (!$arr) { echo "[ ]\n"; return; }
foreach ($arr as $v) { if (is_array($v)) {$run_sub=true; break;} }
if (isset($run_sub)) {
$subs = function ($arr) {
echo "[\n";
foreach ($arr as $sub) {
if (!$sub) { echo "[ ]\n"; return; }
$s = is_int($sub[0]) ? "\t[${sub[0]}": "\t['${sub[0]}'"; unset($sub[0]);
foreach($sub as$v) $s = is_int($v) ? "$s, $v" : "$s, '$v'";
echo "$s],\n";
}
echo "]\n";
};
return $subs($arr);
}
$s = is_int($arr[0]) ? "[${arr[0]}": "['${arr[0]}'"; unset($arr[0]);
foreach($arr as$v) $s = is_int($v) ? "$s,$v" : "$s,'$v'";
echo "$s]\n";
}
@Jeff-Russ
Copy link
Author

offsetSet variation 1

function offsetSet($key, $val, $mass=false, $incr='this', &$add=null)
	{
		if ($mass===false) { $key = array($key=>$val); $incr = $this->incr; }
		else { $incr = $incr==='this' ? $this->incr : $incr; }
		$add = $incr; # just so passed reference gets set no matter what

		foreach ($key as $k=>$val)
		{
			$k = "$k";
			if ($k==='') {
				if ($this->auto_null) $k = $this->key;
				else { $this->seq[] = $val;
					end($this->seq); $this->key = key($this->seq); continue;
				}
			}

			# sortable key: starting with or is numeric:
			if ($k!=0 || $k[0]==='0' || $k[1]==='0' && $k[0]==='-' || $k[0]==='+' ) {
				if (!$incr||!isset($this->seq[$k])){
					$this->key=$k; $this->seq[$this->key]=$val; continue;
				}
				preg_match(self::$ksplit, $k, $c); $num = $c[1]; $end = $c[2]; $add = $incr;
			}
			# smartkey: starting with backtick eg: `key-1 ending`
			elseif ($k[0]==='`' && preg_match('/^`(\w*)([-+].*)?`(.*)$/', $k, $c)) {

				if (!$c[1] || $c[1]==='key') $num = (float)$this->key; #eg: `+1`, `key+1`
				elseif ($c[1]==='max') $num = (float)max(array_keys($this->seq));#eg: `max last`
				elseif ($c[1]==='min') $num = (float)min(array_keys($this->seq));#eg: `min early`
				elseif ($c[1]==='end')  {end($this->seq);  $num=(float)key($this->seq);}#eg: `end z`
				elseif ($c[1]==='reset'){reset($this->seq);$num=(float)key($this->seq);}#eg: `reset a`
				else $num = (float)$this->key;# a failsafe
			
				if (!$c[2]) $add = $incr;
				elseif (is_numeric($c[2])) $add = $c[2];
				elseif ($c[2][0]==='-') $add = $c[2][0]===$c[2][0] ? -1 : -$incr;
				else                    $add = $c[2][0]===$c[2][0] ?  1 :  $incr;
				
				$end = $c[3];
			} else {
				$num = (float)$this->key;
				$add = $incr;
				$end = $k;
			}

			do $num += $add; while( isset($this->seq[(string)($num+0).$end]) );
			$this->key=(string)($num+0).$end; $this->seq[$this->key] = $val;
		}
		return $this->key;
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment