5. Hashes

Introduction

// PHP uses the term 'array' to refer to associative arrays - referred to in Perl
// as 'hashes' - and for the sake of avoiding confusion, the Perl terminology will
// be used. As a matter of interest, PHP does not sport a vector, matrix, or list
// type: the 'array' [Perl 'hash'] serves all these roles

$age = array('Nat' => 24, 'Jules' => 25, 'Josh' => 17);

$age['Nat'] = 24;
$age['Jules'] = 25;
$age['Josh'] = 17;

$age = array_combine(array('Nat', 'Jules', 'Josh'), array(24, 25, 17));

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$food_colour['Apple'] = 'red'; $food_colour['Banana'] = 'yellow';
$food_colour['Lemon'] = 'yellow'; $food_colour['Carrot'] = 'orange';

$food_colour = array_combine(array('Apple', 'Banana', 'Lemon', 'Carrot'),
                             array('red', 'yellow', 'yellow', 'orange'));

Adding an Element to a Hash

$hash[$key] = $value;

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$food_colour['Raspberry'] = 'pink';

echo "Known foods:\n";
foreach($food_colour as $food => $colour) echo "{$food}\n";

Testing for the Presence of a Key in a Hash

// Returns TRUE on all existing entries with non-NULL values
if (isset($hash[$key]))
  ; // entry exists  
else
  ; // no such entry 

// ------------

// Returns TRUE on all existing entries regardless of attached value
if (array_key_exists($key, $hash))
  ; // entry exists  
else
  ; // no such entry 

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach(array('Banana', 'Martini') as $name)
{
  if (isset($food_colour[$name]))
    echo "{$name} is a food.\n";
  else
    echo "{$name} is a drink.\n";
}

// ----------------------------

$age = array('Toddler' => 3, 'Unborn' => 0, 'Phantasm' => NULL);

foreach(array('Toddler', 'Unborn', 'Phantasm', 'Relic') as $thing)
{
  echo "{$thing}:";
  if (array_key_exists($thing, $age)) echo ' exists';
  if (isset($age[$thing])) echo ' non-NULL';
  if ($age[$thing]) echo ' TRUE';
  echo "\n";
}

Deleting from a Hash

// Remove one, or more, hash entries
unset($hash[$key]);

unset($hash[$key1], $hash[$key2], $hash[$key3]);

// Remove entire hash
unset($hash);

// ----------------------------

function print_foods()
{
  // Perl example uses a global variable
  global $food_colour;

  $foods = array_keys($food_colour);

  echo 'Foods:';
  foreach($foods as $food) echo " {$food}";

  echo "\nValues:\n";
  foreach($foods as $food)
  {
    $colour = $food_colour[$food];

    if (isset($colour))
      echo "  {$colour}\n";
    else
      echo "  nullified or removed\n";
  }
}

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

echo "Initially:\n"; print_foods();

// Nullify an entry
$food_colour['Banana'] = NULL;
echo "\nWith 'Banana' nullified\n";
print_foods();

// Remove an entry
unset($food_colour['Banana']);
echo "\nWith 'Banana' removed\n";
print_foods();

// Destroy the hash
unset($food_colour);

Traversing a Hash

// Access keys and values
foreach($hash as $key => $value)
{
  ; // ...
}

// Access keys only
foreach(array_keys($hash) as $key)
{
  ; // ...
}

// Access values only
foreach($hash as $value)
{
  ; // ...
}

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

foreach(array_keys($food_colour) as $food)
{
  echo "{$food} is {$food_colour[$food]}\n";
}

// ----------------------------

// 'countfrom' - count number of messages from each sender

$line = fgets(STDIN);

while (!feof(STDIN))
{
  if (preg_match('/^From: (.*)/', $line, $matches))
  {
    if (isset($from[$matches[1]]))
      $from[$matches[1]] += 1;
    else
      $from[$matches[1]] = 1;
  }

  $line = fgets(STDIN);
}

if (isset($from))
{
  echo "Senders:\n";  
  foreach($from as $sender => $count) echo "{$sender} : {$count}\n";
}
else
{
  echo "No valid data entered\n";
}

Printing a Hash

// PHP offers, 'print_r', which prints hash contents in 'debug' form; it also
// works recursively, printing any contained arrays in similar form
//     Array
//     (
//         [key1] => value1 
//         [key2] => value2
//         ...
//     )

print_r($hash);

// ------------

// Based on Perl example; non-recursive, so contained arrays not printed correctly
foreach($hash as $key => $value)
{
  echo "{$key} => $value\n";
}

// ----------------------------

// Sorted by keys

// 1. Sort the original hash
ksort($hash);

// 2. Extract keys, sort, traverse original in key order
$keys = array_keys($hash); sort($keys);

foreach($keys as $key)
{
  echo "{$key} => {$hash[$key]}\n";
}

// Sorted by values

// 1. Sort the original hash
asort($hash);

// 2. Extract values, sort, traverse original in value order [warning: finds 
//    only first matching key in the case where duplicate values exist]
$values = array_values($hash); sort($values);

foreach($values as $value)
{
  echo $value . ' <= ' . array_search($value, $hash) . "\n";
}

Retrieving from a Hash in Insertion Order

// Unless sorted, hash elements remain in the order of insertion. If care is taken to
// always add a new element to the end of the hash, then element order is the order
// of insertion. The following function, 'array_push_associative' [modified from original
// found at 'array_push' section of PHP documentation], does just that
function array_push_associative(&$arr)
{
  foreach (func_get_args() as $arg)
  {
    if (is_array($arg))
      foreach ($arg as $key => $value) { $arr[$key] = $value; $ret++; }
    else
      $arr[$arg] = '';
  }

  return $ret;
}

// ------------

$food_colour = array();

// Individual calls, or ...
array_push_associative($food_colour, array('Banana' => 'Yellow'));
array_push_associative($food_colour, array('Apple' => 'Green'));
array_push_associative($food_colour, array('Lemon' => 'Yellow'));

// ... one call, one array; physical order retained
// array_push_associative($food_colour, array('Banana' => 'Yellow', 'Apple' => 'Green', 'Lemon' => 'Yellow'));

print_r($food_colour);

echo "\nIn insertion order:\n";
foreach($food_colour as $food => $colour) echo "  {$food} => {$colour}\n";

$foods = array_keys($food_colour);

echo "\nStill in insertion order:\n";
foreach($foods as $food) echo "  {$food} => {$food_colour[$food]}\n";

Hashes with Multiple Values Per Key

foreach(array_slice(preg_split('/\n/', `who`), 0, -1) as $entry)
{
  list($user, $tty) = preg_split('/\s/', $entry);
  $ttys[$user][] = $tty;

  // Could instead do this:
  // $user = array_slice(preg_split('/\s/', $entry), 0, 2);
  // $ttys[$user[0]][] = $user[1];
}

ksort($ttys);

// ------------

foreach($ttys as $user => $all_ttys)
{
  echo "{$user}: " . join(' ', $all_ttys) . "\n";
}

// ------------

foreach($ttys as $user => $all_ttys)
{
  echo "{$user}: " . join(' ', $all_ttys) . "\n";

  foreach($all_ttys as $tty)
  {
    $stat = stat('/dev/$tty');
    $pwent = posix_getpwuid($stat['uid']);
    $user = isset($pwent['name']) ? $pwent['name'] : 'Not available';
    echo "{$tty} owned by: {$user}\n";
  }
}

Inverting a Hash

// PHP offers the 'array_flip' function to perform the task of exchanging the keys / values
// of a hash i.e. invert or 'flip' a hash

$reverse = array_flip($hash);

// ----------------------------

$surname = array('Babe' => 'Ruth', 'Mickey' => 'Mantle'); 
$first_name = array_flip($surname);

echo "{$first_name['Mantle']}\n";

// ----------------------------

$argc == 2 || die("usage: {$argv[0]} food|colour\n");

$given = $argv[1];

$colour = array('Apple' => 'red', 'Banana' => 'yellow',
                'Lemon' => 'yellow', 'Carrot' => 'orange');

$food = array_flip($colour);

if (isset($colour[$given]))
  echo "{$given} is a food with colour: {$colour[$given]}\n";

if (isset($food[$given]))
  echo "{$food[$given]} is a food with colour: {$given}\n";

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach($food_colour as $food => $colour)
{
  $foods_with_colour[$colour][] = $food;
}

$colour = 'yellow';
echo "foods with colour {$colour} were: " . join(' ', $foods_with_colour[$colour]) . "\n";

Sorting a Hash

// PHP implements a swag of sorting functions, most designed to work with numerically-indexed
// arrays. For sorting hashes, the 'key' sorting functions are required:
// * 'ksort', 'krsort', 'uksort'

// Ascending order
ksort($hash);

// Descending order [i.e. reverse sort]
krsort($hash);

// Comparator-based sort

function comparator($left, $right)
{
  // Compare left key with right key
  return $left > $right;
}

uksort($hash, 'comparator');

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

// ------------

ksort($food_colour);

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

// ------------

uksort($food_colour, create_function('$left, $right', 'return $left > $right;'));

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

Merging Hashes

// PHP offers the 'array_merge' function for this task [a related function, 'array_combine',
// may be used to create a hash from an array of keys, and one of values, respectively]

// Merge two, or more, arrays
$merged = array_merge($a, $b, $c);

// Create a hash from array of keys, and of values, respectively
$hash = array_combine($keys, $values);

// ------------

// Can always merge arrays manually 
foreach(array($h1, $h2, $h3) as $hash)
{
  foreach($hash as $key => $value)
  {
    // If same-key values differ, only latest retained
    $merged[$key] = $value;

    // Do this to append values for that key
    // $merged[$key][] = $value;
  }
}

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$drink_colour = array('Galliano' => 'yellow', 'Mai Tai' => 'blue');

// ------------

$ingested_colour = array_merge($food_colour, $drink_colour);

// ------------

$substance_colour = array();

foreach(array($food_colour, $drink_colour) as $hash)
{
  foreach($hash as $substance => $colour)
  {
    if (array_key_exists($substance, $substance_colour))
    {
      echo "Warning {$substance_colour[$substance]} seen twice. Using first definition.\n";
      continue;
    }
    $substance_colour[$substance] = $colour;
  }
}

Finding Common or Different Keys in Two Hashes

// PHP offers a number of array-based 'set operation' functions:
// * union:        array_merge
// * intersection: array_intersect and family
// * difference:   array_diff and family
// which may be used for this type of task

// Keys occurring in both hashes
$common = array_intersect_key($h1, $h2);

// Keys occurring in the first hash [left side], but not in the second hash
$this_not_that = array_diff_key($h1, $h2);

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$citrus_colour = array('Lemon' => 'yellow', 'Orange' => 'orange', 'Lime' => 'green');

$non_citrus = array_diff_key($food_colour, $citrus_colour);

Hashing References

// PHP implements a special type known as a 'resource' that encompasses things like file handles,
// sockets, database connections, and many others. The 'resource' type is, essentially, a
// reference variable that is not readily serialisable. That is to say:
// * A 'resource' may be converted to a string representation via the 'var_export' function
// * That same string cannot be converted back into a 'resource'
// So, in terms of array handling, 'resource' types may be stored as array reference values,
// but cannot be used as keys. 
//
// I suspect it is this type of problem that the Perl::Tie package helps resolve. However, since
// PHP doesn't, AFAIK, sport a similar facility, the examples in this section cannot be
// implemented using file handles as keys

$filenames = array('/etc/termcap', '/vmlinux', '/bin/cat');

foreach($filenames as $filename)
{
  if (!($fh = fopen($filename, 'r'))) continue;

  // Cannot do this as required by the Perl code:
  // $name[$fh] = $filename;

  // Ok
  $name[$filename] = $fh;
}

// Would traverse array via:
//
// foreach(array_keys($name) as $fh)
// ...
// or
//
// foreach($name as $fh => $filename)
// ...
// but since '$fh' cannot be a key, either of these will work:
//
// foreach($name as $filename => $fh)
// or
foreach(array_values($name) as $fh)
{
  fclose($fh);
}

Presizing a Hash

// PHP hashes are dynamic expanding and contracting as entries are added, and removed,
// respectively. Thus, there is no need to presize a hash, nor is there, AFAIK, any
// means of doing so except by the number of datums used when defining the hash

// zero elements
$hash = array();            

// ------------

// three elements
$hash = array('Apple' => 'red', 'Lemon' => 'yellow', 'Carrot' => 'orange');

Finding the Most Common Anything

foreach($array as $element) $count[$element] += 1;

Representing Relationships Between Data

$father = array('Cain' => 'Adam', 'Abel' => 'Adam', 'Seth' => 'Adam', 'Enoch' => 'Cain',
                'Irad' => 'Enoch', 'Mehujael' => 'Irad', 'Methusael'=> 'Mehujael',
                'Lamech' => 'Methusael', 'Jabal' => 'Lamech', 'Jubal' => 'Lamech',
                'Tubalcain' => 'Lamech', 'Enos' => 'Seth');

// ------------

$name = trim(fgets(STDIN));

while (!feof(STDIN))
{
  while (TRUE)
  {
    echo "$name\n";

    // Can use either:
    if (!isset($father[$name])) break;
    $name = $father[$name];

    // or:
    // if (!key_exists($name, $father)) break;
    // $name = $father[$name];

    // or combine the two lines:
    // if (!($name = $father[$name])) break;
  }

  echo "\n";
  $name = trim(fgets(STDIN));
}

// ----------------------------

define(SEP, ' ');

foreach($father as $child => $parent)
{
  if (!$children[$parent])
    $children[$parent] = $child;
  else
    $children[$parent] .= SEP . $child;
}

$name = trim(fgets(STDIN));

while (!feof(STDIN))
{
  echo $name . ' begat ';

  if (!$children[$name])
    echo "Nothing\n"
  else
    echo str_replace(SEP, ', ', $children[$name]) . "\n";

  $name = trim(fgets(STDIN));
}

// ----------------------------

define(SEP, ' ');

$files = array('/tmp/a', '/tmp/b', '/tmp/c');

foreach($files as $file)
{
  if (!is_file($file)) { echo "Skipping {$file}\n"; continue; }
  if (!($fh = fopen($file, 'r'))) { echo "Skipping {$file}\n"; continue; }

  $line = fgets($fh);

  while (!feof($fh))
  {
    if (preg_match('/^\s*#\s*include\s*<([^>]+)>/', $line, $matches))
    {
      if (isset($includes[$matches[1]]))
        $includes[$matches[1]] .= SEP . $file;
      else
        $includes[$matches[1]] = $file;
    }

    $line = fgets($fh);
  }

  fclose($fh);
}

print_r($includes);

Program: dutree

// @@INCOMPLETE@@
// @@INCOMPLETE@@