Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

Printing a hash in a specific order?

by Limbic~Region (Chancellor)
on Mar 15, 2003 at 14:04 UTC ( #243290=perlquestion: print w/ replies, xml ) Need Help??
Limbic~Region has asked for the wisdom of the Perl Monks concerning the following question:

All:
I am currently working on my first OO project and would like to be able to do something like $record->Print; and have it correctly output the record in the proper format. Unfortunately, the object is stored in a hash - and we know that the hash typically comes out differently than the order it went in.

For complete details on the OO project, you can take a look at this link or you can just readmore for the output format I am looking for:

key : /C=US/A=BOGUS/P=ABC+DEF/O=CONN/OU=VALUE1/S=Region/G= +Limbi c/I=_/ type : UR flags : DIRM ADMINM ADIC ADIM Alias-2 : wL_Region Alias-3 : Limbic Region Alias-4 : Limbic._.Region@nowhere.com Alias-5 : Limbic._.Region Alias-6 : Limbic.Region@nowhere.com Alias-7 : ORG Alias-8 : CMP Alias-10 : O=ORG/OU=Some Big Division/CN=Limbic _. Region Alias-11 : Region Alias-12 : Region, Limbic _ Alias-14 : Limbic _. Region at WT-CONN Alias-15 : ex:/o=BLANK/ou=ORG/cn=Recipients/cn=Mailboxes/cn=LRe +gion Alias-16 : WT:Limbic_Region Alias-17 : SMTP:Limbic._.Region@nowhere.com Alias-18 : /o=A.B.C.D./ou=Vermont Ave/cn=Recipients/cn=wt/cn=Li +mbic_Regi on Full Name : Region, Limbic _. Post Office : WT-CONN Description : 999-555-1212 Some Big Company Tel : 999-555-1212 Dept : Some Big Division Location : EMWS Address : 123 nowhere street City : everywhere State : MD Zip Code : 20874 Building : BLAH Building-Code : MD-ABC owner : CONN

What I have currently looks something like this:

sub Print { my $hashref = shift; my ($key) = keys %{$hashref}; # @Parameters truncated for posting purposes only my @parameters = ('key','type','flags','Alias-2','Zip Code','Build +ing','Owner') foreach my $parm (@parameters) { next unless ($hashref->{$key}->{$parm}); my $value; if ($parm eq 'key') { $value = $key; } else { $value = $hashref->{$key}->{$parm}; } if (length($value) >= 62) { printf("%-16s: %s\n\t%s\n",$parm,substr($value,0,62),subst +r($value,62)); } else { printf("%-16s: %s\n",$parm,$value); } } }

My question - is there a better/smarter way to do this? I guess what I am asking is there a generic way to template hash output that I am unaware of? Since this is my first OO project, I really didn't know what I was getting into. I think if I were to start over, I would make my object a complex array instead of hash. Is that also the wrong approach?

Thanks in advance - L~R

Comment on Printing a hash in a specific order?
Select or Download Code
Re: Printing a hash in a specific order?
by artist (Parson) on Mar 15, 2003 at 15:06 UTC
    Hi L~R,
    you wrote:
    Unfortunately, the object is stored in a hash - and we know that the hash typically comes out differently than the order it went in.

    use Tie::IxHash - ordered associative arrays. Here is the sample:

    # Prints this without tie: # apples # oranges # # Prints this with tie: # oranges # apples use Tie::IxHash; tie %menu, 'Tie::IxHash'; $menu{oranges} = 1; $menu{apples} = 2; foreach (keys %menu) { print "$_\n"; }
    From Description of the module:
    This Perl module implements Perl hashes that preserve the order in which the hash elements were added. The order is not affected when values corresponding to existing keys in the IxHash are changed. The elements can also be set to any arbitrary supplied order. The familiar perl array operations can also be performed on the IxHash.

    artist

      artist,
      Thank you very much! I have to look under the hood of this module as I think I have two problems that may make using it a little less than straight forward.

    • The object may change over time so I would know how to order keys that haven't been created yet.
    • If it is truly forcing the order and not performing some other kind of magic, than it is going to hurt performance.

      In reference to point one, I could always just create a place holder for all possible keys, but I am not sure I want to do that since some parameters can exist with empty string as the value and I do not want to accidently create a parameter in a record that didn't previously exist. As far as point two. I have about 150,000 records in in the database - I wouldn't mind adding some overhead for ease of use as long as it doesn't add an extra hour to processing time.

      Thanks again - I will definately check out this module.
      Cheers - L~R

        Hi L~R,
        You can input your data in benchmarking here.

        My benchmark, comparing 200,00 records and each record contain a 50 fields containing small numeric data as key and value, gives the results:

        Benchmark: timing 200000 iterations of with_tie, without_tie...
          with_tie: 98 wallclock secs (96.22 usr +  0.00 sys = 96.22 CPU) @ 2078.61/s (n=200000)
        without_tie: 14 wallclock secs (13.25 usr +  0.00 sys = 13.25 CPU) @ 15094.34/s (n=200000)
                       Rate    with_tie without_tie
        with_tie     2079/s          --        -86%
        without_tie 15094/s        626%          --
        
        And the code is

        use Benchmark qw(cmpthese); use strict; use Tie::IxHash; sub with_tie { tie my %menu, 'Tie::IxHash'; foreach (1..50){$menu{$_} = $_; } } sub without_tie{ my %menu; foreach (1..50){$menu{$_} = $_;} } cmpthese(200000, { with_tie => \&with_tie, without_tie => \&without_tie });
        artist
Re: Printing a hash in a specific order?
by webfiend (Vicar) on Mar 15, 2003 at 15:11 UTC

    If the order is important, then perhaps a hash might not be the best solution. Maybe an array or a pseudo-hash could serve your purposes better?

    Update: Or, you know, what artist said :-)


    I just realized that I was using the same sig for nearly three years.

      webfiend,
      As I said at the bottom of my post. I think if I were to start over, I would use an array (I believe pseudo-hashes are dead for now). I very well may start over since this is my first OO project. Thank you for re-affirming what I already guessed was the most logical solution - starting over.

      Cheers - L~R

      Update: Using an array doesn't really solve the problem, so starting over is not the answer either. Since new parameters can be added to the record once it is retrieved, the same logic for sorting the output of the hash would have to be used to splice the parameter into the array at the right location.

Re: Printing a hash in a specific order?
by fruiture (Curate) on Mar 15, 2003 at 15:13 UTC

    A Hash does not have any order by definition. You speak of "template hash output that I am unaware of", and that can't imply a certain order of the hash keys. You can only generalize by sorting the keys lexically (or however), not "ordering". Your example _knows_ the hash it outputs and provides they keys to be output with their values. A "generic" way can only be generic, a hash is 'generically' without order (actually there is an order that depends on the way the hash is hashed).

    Data::Dumper is a "generic" way of outputting various data structues ;)

    --
    http://fruiture.de
      fruiture,
      I am not looking for a way to store the hash in a specific order. In my post, I acknowledged that hash keys do not come out in the order they are created. I was looking for a way to have the output automatically sorted (via some magic such as artist provided) into a pre-determined format when it came out un-ordered. Thanks for mentioning Data::Dumper - it made me think of something. I may be able to use it if I decide to stay with a hash and not start over.

      Cheers - L~R

        I was looking for a way to have the output automatically sorted [...] into a pre-determined format when it came out un-ordered.

        that's sort then. It depends on what your "pre-determined format" means.(?) I was confused by your example because it dictated the keys to be output in some order (just as the example output displayed no generic order but some specific one) and that's of course impossible to be done generically unless you specify this order without enumarating it. So we end up with sort() or some Hash-tying module that actually stores order of insertion (as you've been pointed to).

        --
        http://fruiture.de
Re: Printing a hash in a specific order?
by Anonymous Monk on Mar 15, 2003 at 15:55 UTC

    What does this do?

    my ($key) = keys %{$hashref};
      Anonymous Monk,
      It is magic - bad magic, but magic none the less. Without showing you my very poorly written OO code. Basically, I am creating a complex hash (HoHo...). There is really only one key to the hash at any one time - which points further down the complex structure. So $hashref is a scalar that is a reference that points to the actual hash. The %{$hashref} is dereferencing the reference it to make it look like a real hash. The keys extracts the one and only key that is there. Putting the parens around the my ($key) part changes the context from scalar (which would have returned the number of keys i.e. 1) to a list context (which returns the one and only key).

      Cheers - L~R

        Here's a more efficient idiom for grabbing a single key from a hash reference:

        my $key = each %{$hashref};

        The approach you're using with my($key) = keys %{$hashref}; essentially generates a list of the keys in memory, assigns the first element in the list to $key, and then throws rest of the list out. If the size of the hash is large this could be wasteful, but even with small hashes there is a noticeable difference between the two idioms when benchmarking.

        Dan Kubb, Perl Programmer

        Updated: Made minor corrections noted by Limbic-Region

Re: Printing a hash in a specific order?
by artist (Parson) on Mar 15, 2003 at 16:23 UTC
    Another approach
    my @records; $records[1] = { a => 1, d=>4, b=> 2, c=> 3}; $records[2] = { a => 11, d=>14, b=>12}; # didn't define 'c' here $records[3] = { a => 123, d=>10, e=>200, c=> 21, d=>9888 }; # differen +t order, 'b' absent my @keys = qw( a c b); # only want to print a c and b foreach my $record (@records){ print join "\t" => map { $_, length($record->{$_}) < 3 ? $record->{$_} : substr($record->{$_},0,1)} grep { defined $record->{$_}} @keys, "\n"; print "\n"; }
    Output:
    a	1	c	3	b	2
    a	11	b	12
    a	1	c	21
    
    artist
Re: Printing a hash in a specific order?
by poj (Priest) on Mar 15, 2003 at 16:49 UTC
    This prints a single record, or do you want to print many records in a defined order ?
    #!/usr/bin/perl -w use strict; # test HOH my %test = ( 'rec1' => { 'key'=>"rec1", 'a' =>"11" x 50, 'b' =>"1b1b1b", 'c' =>"c1c1c1c1c", 'z' =>"zz11zz11zz" }, 'rec2' => { 'key'=>"rec2", 'a' =>"22" x 40, 'b' =>"2b2b2b", 'c' =>"c2c2c2c2c", 'x' =>"xx22xx22xx" } ); my $obj = new mydata(\%test); $obj->Print('rec2'); $obj->Print('rec1'); package mydata; sub new { my (undef,$self)=@_; bless $self; } sub Print { my ($self,$key) = @_; my $record = $self->{$key}; my @parameters = ('key','a','z','c','b','x'); foreach my $parm (@parameters) { next unless ($record->{$parm}); my $value = ($parm eq 'key') ? $key : $record->{$parm}; printf "%-16s: %s\n",$parm,substr($value,0,62) ; printf "\t%s\n",substr($value,62) if length $value > 62; } }
    poj
Re: Printing a hash in a specific order?
by thpfft (Chaplain) on Mar 15, 2003 at 18:30 UTC

    If you just want the output to look tidy, or to be ordered in a predictable way, then a reasonably simple lexical sort will probably serve. That will get you as far as putting all the alias-* fields next to each other, but if your goal is to make the information human-friendly - ie ordered in an arbitrary way - then the answer will have to be a form of template.

    You could consider a full templating system like the Template Toolkit - the initial investment of time would almost certainly pay off later - but for your present requirements all you need is a sort of master sequence that will serve as a guide:

    key type flags Alias-.* Full Name Post Office Description ... Building-.* owner

    Then you just step through the template dropping the right data into your output string in response to each line. Implementing the wildcards will be a bit tricky, but you could use something like the following wildly untested bit of pseudo-perl to obey regexes in the template:

    my $output; while (<template>) { chomp; for (grep { /$_/ } keys %{ $data{$key} }) { $output .= "$_: " . $data{$key}{$_} . "\n"; } }

    which would of course be bristling with taint- and sanity-checks before it made it out into the world :)

Re: Printing a hash in a specific order?
by dga (Hermit) on Mar 15, 2003 at 20:53 UTC

    Something which has not been explicitly said, but which was alluded to in the Template post goes like this.

    my @keys_in_order = qw( key type flags Alias-2 ... ); #declared someplace so it doesn't get assigned each pass sub Print { my $hashref=shift; foreach my $k ( @keys_in_order ) { my $value=$hashref->{$k} if defined $hashref->{$k}; #rest of processing here } }

    This way the keys come out in order if something defined was put in that spot in the hash. (this could be changed to just if or if exists depending on what values are really there and which aren't, decided by the application paramaters. This allows values to have 0 and still print in the current incarnation.) So changing the key order printout is as easy as changing the array which says which order to print out the hash values. You get the convienience of a hash which remains a fully optimal hash until printed then prints out in an arbritrary order for a minor cost.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://243290]
Approved by grinder
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others imbibing at the Monastery: (8)
As of 2014-10-01 21:07 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    What is your favourite meta-syntactic variable name?














    Results (38 votes), past polls