Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling

Turning a hash into a line of links

by OfficeLinebacker (Chaplain)
on Dec 12, 2006 at 20:06 UTC ( #589389=perlquestion: print w/replies, xml ) Need Help??
OfficeLinebacker has asked for the wisdom of the Perl Monks concerning the following question:

Greetings, esteemed monks!

I think this is something that might lend itself to `map,` perhaps in conjunction with `join,` though I am not very comfortable with `map.` This code is for a CGI script. Basically, I have a hash, the keys of which are part of the text to be displayed for each link, and the values of which are the URLs. The URLs do change every once in a blue moon, and entries may be added or subtracted (also infrequently), and I'd like to keep that information in a variable and generate the list of links programmatically. Here's what I have so far; note the leading space in the first key, for sorting purposes:
use strict; use warnings; use CGI qw/:all *table'/; use CGI::Carp qw(fatalsToBrowser warningsToBrowser); ... my %sections=(" DMA"=>'dma',FST=>'dma/fst',MRA=>'dma/mra',BKS=>'bks',M +SU=>'dma/msu',FMA=>'dma/fma',FOMC=>'dma/FOMC'); ... print "<h3 align='center'>"; foreach my $s (sort keys(%sections)){ print a({-href => "/$sections{$s}"}, "$s HOME")," |"; } print "</h3>"; ...
Clearly, I have one extra " |" after the last link, which I can fix, but I'm a junkie for "elegant" solutions, thus this node. Unfortunately, since both text and URL change, I don't believe I can use "The Distributive Property of HTML Shortcuts," which I have used to great advantage and with great glee in the past (and indeed in other parts of the program excerpted above).

As always, any and all suggestions welcome.


I like computer programming because it's like Legos for the mind.

Replies are listed 'Best First'.
Re: Turning a hash into a line of links
by brian_d_foy (Abbot) on Dec 12, 2006 at 21:45 UTC

    If %sections won't be large (and by now thinking it won't, it will be), the natural answer to "How do I put this between elements" is join:

    print join "\n| ", map { a( {-href => "/$sections{$_}"}, "$_ HOME" ) } sort keys %sections;

    I put a newline before the separator so it shows up at the beginning on the line and the link follows it. That makes for easy reading when you have to look at the source (for instance, to verify it's what you said it should be):

    Good luck :)

    brian d foy <>
    Subscribe to The Perl Review
Re: Turning a hash into a line of links
by liverpole (Monsignor) on Dec 12, 2006 at 20:21 UTC
    Hi OfficeLinebacker,

    How about something like this? ...

    my @sorted = sort keys(%sections); for (my $i = 0; $i < @sorted; $i++) { ($i > 0) and print "|"; my $s = $sorted[$i]; print a({-href => "/$sections{$s}"}, "$s HOME"); }

    It prints a leading | instead, but not before the very first link.

    Update:  If you really want to do it with map, I'd suggest something like this:

    print join("|", map { a({-href => "/$sections{$_}"}, "$_ HOME") } sort + keys %sections);

Re: Turning a hash into a line of links
by johngg (Abbot) on Dec 12, 2006 at 21:25 UTC
    How about using map to construct a list of tags and then change the default list separator to the pipe symbol and just print the array.

    #!/usr/bin/perl -l # use strict; use warnings; use CGI qw/:all *table'/; use English q{-no_match_vars}; my %sections = ( q{ DMA} => q{dma}, FST => q{dma/fst}, MRA => q{dma/mra}, BKS => q{bks}, MSU => q{dma/msu}, FMA => q{dma/fma}, FOMC => q{dma/FOMC}); print q{<h3 align='center'>}; my @aTags = map { a({-href => qq{/$sections{$_}}}, qq{$_ HOME}) } sort keys %sections; { local $LIST_SEPARATOR = q{|}; print qq{@aTags}; } print q{</h3>};

    Here's the output, the script runs with the -l flag.

    <h3 align='center'> <a href="/dma"> DMA HOME</a>|<a href="/bks">BKS HOME</a>|<a href="/dma +/fma">FMA HOME</a>|<a href="/dma/FOMC">FOMC HOME</a>|<a href="/dma/fs +t">FST HOME</a>|<a href="/dma/mra">MRA HOME</a>|<a href="/dma/msu">MS +U HOME</a> </h3>

    This seems to do what you want.



    Update: Added output.

Re: Turning a hash into a line of links
by OfficeLinebacker (Chaplain) on Dec 12, 2006 at 22:47 UTC
    You guys are just too good! ++everyone.
    So shortly after I posted I tried
    my @la; foreach my $s (sort keys(%sections)){ push @la,a({-href => "/$sections{$s}"}, "$s HOME"); } print join(" |",@la);
    which does the trick, and it seems the loop is just a longer (and perhaps clearer) way of saying (as Mr. Foy so eloquently stated)
    my @la=map { a( {-href => "/$sections{$_}"}, "$_ HOME" ) } sort keys % +sections;
    As far as formatting of the source HTML, I tend to do a
    $\ = "\n";
    near the top of just about every program, especially CGI scripts, for precisely the reason Mr. Foy stated. As the documentation for states,
    "By default, all the HTML produced by these functions comes out as one long line without carriage returns or indentation. This is yuck, but it does reduce the size of the documents by 10-20%."
    (my emphasis)

    Thanks again,

    I like computer programming because it's like Legos for the mind.
Re: Turning a hash into a line of links
by jbert (Priest) on Dec 13, 2006 at 08:56 UTC
    Other people have mentioned join, which is the right tool for this job. You're also right in thinking of map as being equivalent to the explicit loop. They are actually very similar - in both cases you can actually modify the loop variable (or map variable) and change the underlying array.

    Personally, I tend to use map< for "making a new X for every y in the array", i.e. when I want to create a different list of N items, as is the case here. If I want a side-effect (e.g. printing, or calling a function to process every y in the array) I tend to write the explicit loop for clarity. map has that 'pipeline' feel to it.

    You mentioned having a leading space on one of the keys, in order to force the sort order.

    This is ingenious, but leaves a space in your output and doesn't scale easily to giving your greater control over the sort order. If you want to do that, one way to extend your idea would be to add a prefix, say A_, B_, etc, to each key, in the order you want them displayed.

    You can then include the prefix stripping as part of your processing, which brings us to map's close relative (evil twin?) apply. This is almost identical to map, but works on a copy of the items from the array, so it appropriate when you want to change the value of $_ in the processing step (as we do here, to strip off the sorting tag at the front):

    #!perl -w use strict; use List::MoreUtils qw/apply/; my %sections=( A_DMA => 'dma', B_FST => 'dma/fst', C_MRA => 'dma/mra', D_BKS => 'bks', E_MSU => 'dma/msu', F_FMA => 'dma/fma', G_FOMC => 'dma/FOMC', ); print join("\n", apply { s/^\w_//; } sort keys %sections);
    Note that you need to pull in apply from List::MoreUtils, where you'll also find lots of other goodies in a similar vein, allowing you to replace loops with simpler, hopefully clearer, code.
      I think that mangling the keys by adding a prefix could quickly get out of hand and become difficult to manage, with strategies required if the number of sections goes over 26 etc. If you have one particular key, as in the OP, that has to come first then sort on whether you have found that key before the lexical sort, like this

      #!/usr/local/bin/perl -l # use strict; use warnings; my %sections = ( DMA => q{dma}, FST => q{dma/fst}, MRA => q{dma/mra}, BKS => q{bks}, MSU => q{dma/msu}, FMA => q{dma/fma}, FOMC => q{dma/FOMC}); my $thisKeyFirst = q{DMA}; my @sortedKeys = map { $_->[0] } sort { $b->[1] <=> $a->[1] || $a->[0] cmp $b->[0] } map { [$_, $_ eq $thisKeyFirst] } keys %sections; print for @sortedKeys;

      which produces


      It seems to me that it would be easier to manage that way. It you had a few special keys that fell into this category this method would still work but with a hash to hold the order of the specials, highest value first and keys not in specials hash getting zero. Something like (not tested)

      my %specials = ( DMA => 2, MSU => 1); ... map { [$_, exists $specials{$_} ? $specials{$_} : 0] } keys %sections;



        I think at that point, it would be better to just maintain a real order, via a list, and associating values as part of an array ref.
        my @sections = ( [DMA => 'dma'], [FST => 'dma/fst'], [MRA => 'dma/mra'], [BKS => 'bks'], [MSU => 'dma/msu'], [FMA => 'dma/fma'], [FOMC => 'dma/FOMC'], ); my @la = map { a( {-href => "/$_->[1]"}, "$_->[0] HOME" ) } @sections; print join "\n|", @la;
        In fact that's probably better than the key-mangling for this number of keys too.
Re: Turning a hash into a line of links
by f00li5h (Chaplain) on Dec 13, 2006 at 11:30 UTC

    Well, hashes don't have any order, which may be annoying, if you're trying to get a navigation going (since they may move about, and won't appear in the order you want) so i stuffed them into a list-o-hashrefs

    I think a nice way to do this would be with Template Toolkit

    This will give you something a bit like

    well, lookie here: DMA |FST |MRA |BKS |MSU |FMA |FOMC

    You may want to read Introduction to Template Toolkit (part 3) from merlyn's neat Linux Magazine Columns.

    Also, here's a vi regex to make your list into a list of hash refs: :s/\(\w\+\)=>\([^,]\+\)/{label=>'\1',uri=>\2}\r/g

    @_=qw; ask f00li5h to appear and remain for a moment of pretend better than a lifetime;;s;;@_[map hex,split'',B204316D8C2A4516DE];;y/05/os/&print;
Re: Turning a hash into a line of links
by SFLEX (Chaplain) on Dec 12, 2006 at 21:26 UTC
    This would be one way I would do it.
    my $some_times = ''; my @sec_keys = sort keys(%sections); foreach my $s (@sec_keys){ print $some_times, a({-href => "/$sections{$s}"}, "$s HOME"); $some_times = " |" if (!$some_times); }
Re: Turning a hash into a line of links
by OfficeLinebacker (Chaplain) on Dec 13, 2006 at 13:39 UTC
    Hi guys. Thanks again for all the comments. While I was playing with this, I had the "old way" (with the extra pipe) and the "new way" printing at the same time, and the top line was longer than the second, and it looked cool. So I decided to try to divise a programmatic way to split the line into two, with the top part never having less elements than the second. My algorithm was to take the final string, count the number of pipes in it, and then turn the "middle" one (and, if no clear "middle," then skew towards later in the string) into a br tag. Here's what I came up with:
    my $homes=join(" |",@la); #print $homes; my @pipes; my $pos; while ($homes =~ m/\|/g){ push @pipes, pos($homes)-1; } my $numps=scalar(@pipes); my $ptr= $numps-int($numps/2); #no ceil() function in perl substr($homes,$pipes[$ptr],1,"<br>");#not catching rv--is that bad? print $homes;
    I know there are probably better ways, but I kind of had fun figuring this out in a way that's flexible if sections are added/subtracted.

    I like computer programming because it's like Legos for the mind.
      A slightly different, but not necessarily better, way would be to count the pipes using tr, like this

      my $numps = ($homes =~ tr{|}{});

      and then find the "middle" one's position by using index in a loop

      my $middle = int($numps / 2) + 1; my $pos = -1; my $found = 0; while (1) { $pos = index $homes, q{|}, $pos; last if ++ $found == $middle; $pos ++; } substr $homes, $pos, 1, q{<BR>};

      As you say, it is fun trying these things out. As for checking return values, I should perhaps check that index doesn't return -1 and that substr does return the pipe that I think I'm replacing; if this was production code I probably would :)



        tr//--Now there's another element of perl with which I am not very familiar. When I was devising my plan, I decided that I wanted to only loop through/traverse the string once, thus the array of positions of the pipes. Does tr not loop through the string? I'll have to look that up. I was focused on the thought that I will have to loop through the string once to count all the pipes; might as well save the positions while I am there so I don't have to go back through to "find" the middle one. Yeah I had to use the number of elements in the @pipes array to do some funky arithmetic, but only one time through the string.

        I've seen the q{} contruct more than once now. Do you guys use a lot of strings that contain embedded quotes? What are the relative merits of q{hi} vs 'hi'?


        UPDATE: I figured out what tr/// does. I kept bouncing back and forth between tr///, which says "same as y///" and y///, which says "same as tr///." Finally saw the part that says "see perlop." Thanks.

        I like computer programming because it's like Legos for the mind.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://589389]
Approved by Joost
Front-paged by brian_d_foy
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others surveying the Monastery: (5)
As of 2018-06-24 19:47 GMT
Find Nodes?
    Voting Booth?
    Should cpanminus be part of the standard Perl release?

    Results (126 votes). Check out past polls.