http://www.perlmonks.org?node_id=238721
Description:

Suppose you have a hash of replacements to perform.

my %replace = ( qw/ COM SEC MOB HIS MOC COM ICM1 INO ICM2 INO EU HIS CY GRE AE MOB IN ICC GR GRE MH MOC CO MOC MO HIS / );

That is, if you see 'COM', you must replace it with 'SEC'. For a real world example, my company restructures itself internally every once in a while, and while the people remain the same, the departments change name, merge and split. The above example represents the fields in database records to be updated to reflect the change.

Looking carefully, one sees that the transfer COM -> SEC has to be applied before MOC -> COM. If performed in the wrong order, MOC will be transformed to COM and thence to SEC, which would be a Bad Thing.

We have to look for the case where a hash value x (the replacement) matches a hash key y (what is to be replaced). If so, the tranformation of hash key y needs to be performed first, to clear the way for the hash key (whatever it is) to transform itself to x in a subsequent pass.

Thus, we want an array of references to tranform hashes. If we get the order right, we are guaranteed of not clobbering anything in our transforms.

Another problem to worry about is in the case of COM -> SEC and SEC -> COM. In this case we have a loop. The only way to solve this problem is to weaken the loop by introducing an intermediate step: COM -> ZZZ, SEC -> COM, ZZZ -> SEC (where ZZZ is a random string guaranteed not to exist among the set of transforms). I didn't run into this problem (I should be thankful for small mercies) but I added the code to at least detect the problem. Solving it is left as an exercise to the reader.

I'm interested in feedback. Is there a better way? Simpler? Non-recursive? A better way of dealing with the @order array? (I don't like passing it as a parameter, I think it would be more elegant to say my @order = demangle( \%replace). And demangle is a silly name, but I can't think of a good action/verb that describes what I'm doing. Hell, even a better title for this snipper would help (something that will help people search for it -- thanks extremely).

#! /usr/bin/perl -w

use strict;

my %replace = ( qw/
    COM  SEC
    MOB  HIS
    MOC  COM
    ICM1 INO
    ICM2 INO
    EU   HIS
    CY   GRE
    AE   MOB
    IN   ICC
    GR   GRE
    MH   MOC
    CO   MOC
    MO   HIS
/);

my @order;
demangle( \%replace, \@order );

sub demangle {
    my $r     = shift;
    my $order = shift;

    my %invert;
    @invert{ values %$r } = keys %$r;

    my( %okay, %collide );

    for my $key( sort keys %$r ) {
        if( exists $invert{$key} ) {
            $collide{$key} = $r->{$key};
        }
        else {
            $okay{$key} = $r->{$key};
        }
    }
    unshift @$order, \%okay;

    if( %collide ) {
        my @loop_keys = sort keys %collide;
        my @loop_vals = sort values %collide;
        my $is_loop = 1;
        for( my $n = 0; $n < scalar @loop_keys; ++$n ) {
            if( $loop_keys[$n] ne $loop_vals[$n] ) {
                $is_loop = 0;
                last;
            }
        }
        if( $is_loop ) {
            warn "\t$_\t$collide{$_}\n" for sort keys %collide;
            die "loop in transforms detected, bailing out\n";
        }
        demangle( \%collide, $order );
    }
}

my $pass = 0;
for my $r( @order ) {
    ++$pass;
    print "Pass $pass\n";
    for my $key( keys %$r ) {
        print "\t$key -> $r->{$key}\n";
    }
}
__END__
# produces:
Pass 1
        COM -> SEC
Pass 2
        MOB -> HIS
        MOC -> COM
Pass 3
        ICM1 -> INO
        ICM2 -> INO
        EU -> HIS
        CY -> GRE
        IN -> ICC
        GR -> GRE
        MH -> MOC
        CO -> MOC
        AE -> MOB
        MO -> HIS