Skip to content

Instantly share code, notes, and snippets.

@Jeff-Russ
Last active January 27, 2023 14:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jeff-Russ/3015486db8eb2dc69f2763e368b340a2 to your computer and use it in GitHub Desktop.
Save Jeff-Russ/3015486db8eb2dc69f2763e368b340a2 to your computer and use it in GitHub Desktop.
PHP: numeric array keys

PHP's Numeric & String Key Type Juggling

Arrays in PHP treat integer and string integers synonymously. If you set $a['1'] = '1' and access $a[1] you will get '1'. This is because php juggled your string key to an integer before assigning it; the assigment was actually made to an integer key. PHP also juggles in the process of accesssing too which means if you try to access [1] with ['1'] it will work. Therefore there is no real distinction between string integers and real integer, you can use them interchangably.

Remember that '1.0' does not equal '1' or 1 with strict comparison. This is true of array keys as well: you can have both ['1.0'] and ['1'] and they would be different elements but not [1] and ['1'].

$arr = array(0, 1);
$arr['0'] = 'changing value of [0] here';
$arr['1.0'] = 'adding a new element at ['1.0]';

Possibly the most troubling behavior in PHP array keys happens when you try to use real floating point numbers as keys.

$arr = array(1=>1);
$arr[1.1] = "we just clobbered [1] !!";

Floating point strings are converted to integer key, dropping the decimal component!! This is is pretty good reason to wrap all keys in string when accessing or assigning to be safe since this would retain floats and not effect integers.

Unwanted Behaviors

Wrapping in strings works but not when you actually want ['1.0'] to be the same as [1]. Also...

  1. You may not want to have both ['1'] and ['1.0'] allowed distinctly.
  2. You may not want to have both ['1.0'] and ['1.00'] allowed distinctly. Trailing zeros can pose an issue.
  3. You probably don't want php to treat [1.5] as if you typed [1].

Sanitizing Numeric Keys

If you are making an ArrayObject class you would be able to sanitize numeric keys. You could also do this with normal arrays, wrapping assigment and accessing in functions.

If you add a number to a numeric string you will get either a float or an int depending on whatever would not loose resolution. If you add "1.5" + 0 you will get a float. Converting 1.0 to string will drop the decimal which is what we want.

$var = (string)('1.0' + 0); // $var is '1'
$var = (string)('1.5' + 0); // $var is '1.5'

This works rather well to solve all three issues! But If if you have a non-numeric string you would have just mangled you key. You could wrap the above in and if statement:

if ( is_numeric($key) ) {
  $key = (string)($key + 0);
}

This works fine but if you are really obsessed in optimizing your code performance you might want to avoid the call to is_numeric. If you loose compare with == or != the result of above with the original they will be 'equal' if the original was a float, int, or numeric string (int or float)!

if ( ($n=(string)($key + 0)) == $k) $key = $n;

Or if you want to reject any non-numerics completely you could do the following and then just use $n and not $key subsequently.

if ( ($n=(string)($key + 0)) != $key) trigger_error('only numeric keys!');
// use $n ....

ksort and Key Type

ksort has different behaivor if you have non-numeric string keys in your array. Without them, it has a very nice characteristic:

$arr = [
    '0.0'=>'0.0',
     2   => 2,
    '1'  => 1,
    '3'  => 3,
     5   => 5,
    '11' =>'11',
    '6'  => 6,
    '5.1'=>'5.1',
     8   => 8,
    '9'  => 9,
    '10' => 10, 
];
ksort($arr);
var_dump($arr);

This results in a nice numeric order, regardless of whether you have a integer, string integer or string float:

array (
  '0.0'=> '0.0',
   1   => 1,
   2   => 2,
   3   => 3,
   5   => 5,
  '5.1'=> '5.1',
   6   => 6,
   8   => 8,
   9   => 9,
  10   => 10,
  11   => '11',
)

This could be very useful, unlike the havoc that occurs when you mix in some non-numeric string keys:

$arr = [
    '0.0'=>'0.0',
     2   => 2,
    '1'  => 1,
    'what?'=>'what?',
    '3'  => 3,
     5   => 5,
    'eight'=>'eight',
    '11' =>'11',
    '6'  => 6,
    '5.1'=>'5.1',
     8   => 8,
    '9'  => 9,
    '10' => 10,  
];

Outputs:

array (
  '0.0' => '0.0',
  '5.1' => '5.1',
  'eight' => 'eight',
  'what?' => 'what?',
  1 => 1,
  2 => 2,
  3 => 3,
  5 => 5,
  6 => 6,
  8 => 8,
  9 => 9,
  10 => 10,
  11 => '11',
)

Not so nice: it places string numerics (floats, since int strings become ints) first, then alpha strings, then integers. Interesting, all that matters to retain this goodness is that your first character is a numeric:

$arr = [
    '0.0'=>'0.0',
     2   => 2,
    '1'  => 1,
    '5.5'=> 5,
    '3'  => 3,
     5   => 5,
    '4:n'=>'4:n',
    '11' =>'11',
    '6'  => 6,
    '5.x'=>'5.x',
     8   => 8,
    '9'  => 9,
    '10' => 10,
];

Outputs:

array (
  '0.0' => '0.0',
   1 => 1,
   2 => 2,
   3 => 3,
  '4:n' => '4:n',
   5    => 5,
  '5.5' => 5,
  '5.x' => '5.x',
   6 => 6,
   8 => 8,
   9 => 9,
  10 => 10,
  11 => '11',
)

This might be handy. You could filter out bad keys with:

if ( ($n=(string)($k+0)) == $k) $k = $n;
elseif ($k==='' || !ctype_digit ("$k"[0])) trigger_error('key must start with numeric');

but this would block negative number or explicit positive numbers ("-1:3") So you might want:

if ( ($n=(string)($k+0)) == $k) $k = $n;
elseif (!preg_match ('/^[-+]?[0-9].*$/',$k="$k")) trigger_error('key must be or start with number');

This is better for another reason: it's safer since $k="$k" ensures you are left with a string no matter what, which is also important for avoiding errors with preg_match.

NOTE that the above would convert '1.1.0' or '1.1.5' to '1.1' so this approach might not be suitable for version numbers. This is because those strings DO juggle to numbers, both with +0 and the comparison with == just drop the final dot and number. If you version number style strings you might want to sanitize with:

if (is_numeric($k)): $k = (string)($k+0); 
elseif ( !preg_match('/^[-+]?[0-9].*$/',$k="$k") ):
    trigger_error("keys must be or start with number");
endif; 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment