mnperez has asked for the wisdom of the Perl Monks concerning the following question:



I'm looking for a way to sort a hash that uses an IP address as the key, such that the list would be like:

1.x.x.x
2.x.x.x
3.x.x.x

I searched around and found an example that did it as an array, but I am not sure how to apply that to a hash (presumably in a foreach statement).
- Mike

Replies are listed 'Best First'.
Re: Sorting a hash of IP addresses?
by davorg (Chancellor) on Dec 05, 2001 at 14:54 UTC

    Well, you can't sort a hash, as hashes are always unsorted. But presuming that you want to process the hash values in sorted order, you can do something like this:

    foreach (sort ipsort keys %ip) { # each sorted key will be in $_ } sub ipsort { my @a = split /\./, $a; my @b = split /\./, $b; return $a[0] <=> $b[0] || $a[1] <=> $b[1] || $a[2] <=> $b[2] || $a[3] <=> $b[3]; }

    Update: As hakkr points out below you can certainly have a variable that looks and acts like a sorted hash by using Tie::IxHash. But being pedantic, it isn't actually a hash. It's a Perl object (actually a reference to an array) that you can interact with via an interface that looks very much like a hash :)

    And, anyway, it sorts by insertion order, which isn't what the OP wanted.

    --
    <http://www.dave.org.uk>

    "The first rule of Perl club is you do not talk about Perl club."
    -- Chip Salzenberg

      may I add that a Schwartzian Transform might help speeding up the sort. Then the split doesn't have to be done over and over again. This leads to

      my @sorted_keys = map {$_->[0]} sort {$a->[1] <=> $b->[1] || $a->[2] <=> $b->[2] || $a->[3] <=> $b->[3] || $a->[4] <=> $b->[4]} map { [ $_, split /\./ ] } keys %ip; foreach (@sorted_keys) { # do something }

      Instead of splitting one could also use the MZSanford's suggestion and sort on a zeropadded string in the transformation.

      Update:
      The code above was written to resemble as closely as possible davorg's original subroutine. For better solutions (in terms of performance) see blakem's and davorg's responses to my post.

      -- Hofmator

        You could simplify the comparison by upgrading your transformation. Instead of just splitting it into four pieces, why not convert it to dotless notation (i.e. the decimal representation of the underlying 32 bits). Comparing ips in dotless notation is trivial... a single <=> is all you need.

        In general it makes sense to push the complexity into the transformation since it is only done once for each element in the list.

        Something like: (warning untested)

        my @sorted_keys = map {$_->[0]} sort {$a->[1] <=> $b->[1]} map { [ $_, unpack("N",pack("C4",split(/\./,$_))) ] + } keys %ip;
        * I borrowed the ip => dotless conversion from Fletch's golf entry

        -Blake

      my %ipsort; use Tie::IxHash; tie %ipsort, "Tie::IxHash";

      This will give you a hash sorted by insertion order.

Re: Sorting a hash of IP addresses?
by gbarr (Monk) on Dec 05, 2001 at 18:34 UTC
    A simple solution would be to pack the IPs as if you were passing them to the socket routines. In this format the IP will be 4 bytes, where each byte is the numbers from the IP
    use Socket; @sortedKeys = map { inet_ntoa($_) } sort map { inet_aton($_) } keys %hash;
Re: Sorting a hash of IP addresses?
by MZSanford (Curate) on Dec 05, 2001 at 14:51 UTC
    If you want to sort on complete addresses, you could use the complete addresses as keys, and then pass a coderef to sort that 0 pads the octcets before comparing. If you wanted a by-range, i guess you could make a HoH with the keys of each portion of the IP address , so 1.2.3.4 would be $hash{1}{2}{3}{4} ... what purpose are these being sorted for ? If it is part of a bigger app, i may be able to offer a bit more. Hope that helps.
    $ perl -e 'do() || ! do() ;' Undefined subroutine &main::try
Re: Sorting a hash of IP addresses?
by strat (Canon) on Dec 05, 2001 at 16:41 UTC
    Or another solution with Schwartzian Transform: I can just give you a list of sorted keys:
    my @sortedKeys = map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, join("", # xx.XXX.xx.X => xxxXXXxxxXXX map { sprintf("%03d", $_) } ( /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ ) ) ] } keys %hash;
    But this solution might not be the fastest one :-)