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

I would like to sort based on an attribute. I am working on an automatic 
derived object publishing system using ClearCase for sharing do's across 
different types of automated build systems of which some may not be using 
ClearCase. Since my perl program is over a 1000 lines, I thought an 
existing example from the xmltwig site would help. 

I added an attribute called vob.  Note: not all names have this attribute
because it also has to publish to a simple directory structure.

<stats><player vob="d" ><name>Houston, Allan</name><g>69</g></player>
<player vob="z" ><name>Sprewell, Latrell</name><g>69</g></player>
<player vob="a" ><name>Ewing, Patrick</name><g>49</g></player>
<player vob="none" ><name >Johnson, Larry</name><g>57</g></player>
<player vob="g" ><name>Camby, Marcus</name><g>48</g></player>
<player vob="d" ><name>Thomas, Kurt</name><g>67</g><ppg>7.9</ppg></player>
<player vob="none" ><name >Ward, Charlie</name><g>59</g><ppg>7.5</ppg></player>
<player vob="none" ><name>Wallace, John</name><g>55</g><ppg>6.6</ppg></player>
<player><name>Childs, Chris</name><g>61</g><ppg>5.3</ppg></player>
<player><name>Duncan, Tim</name><g>66</g><ppg>23.1</ppg></player>
<player><name>Robinson, David</name><g>68</g><ppg>17.1</ppg></player>

</stats>

I would like them to be ordered like this:
No attribute or vob=none
--------------
Childs, Chris
Duncan, Tim
Robinson, David
Johnson, Larry    ....none
Ward, Charlie     ....none
Wallace, John     ....none

a,d,g,none,z
--------------
Ewing, Patrick    ....a
Houston, Allan    ....d
Thomas, Kurt      ....d
Camby, Marcus     ....g
Sprewell, Latrell ....z


Here is the example for sorting.
In the example below, the sort command is numerically sorting on first child.

My current plan is as follows:
Run through the entire players array collecting the values for the vob attribute,
and then sort and make unique my attribute values. While looping through the 
unique vob attribute values, loop through the players array testing
to see if each player matched the attribute.

I was hoping for something more elegant.

Any suggestions would be appreciated.
Thanks,

Patrick

#########################################################################
#                                                                       #
#  This first example shows how to create a twig, parse a file into it  #
#  get the root of the document, its children, access a specific child  #
#  and get the text of an element                                       #
#                                                                       #
#########################################################################

use strict;
use XML::Twig;

my $field= $ARGV[0] || 'name';
my $twig= new XML::Twig;

$twig->parsefile( "nba.xml");    # build the twig
my $root= $twig->root;           # get the root of the twig (stats)
my @players= $root->children;    # get the player list

                                 # sort it on the text of the field
my @sorted= sort {    $b->first_child( $field)->text 
                  <=> $a->first_child( $field)->text }
            @players;
                                 
print '<?xml version="1.0"?>';   # print the XML declaration
print '<!DOCTYPE stats SYSTEM "stats.dtd" []>';
print '<stats>';                 # then the root element start tag

foreach my $player (@sorted)     # the sorted list 
 { $player->print;               # print the xml content of the element 
   print "\n"; 
 }
print "</stats>\n";              # close the document

Replies are listed 'Best First'.
Re: XML::Twig -- sorting by attribute
by mirod (Canon) on Jan 31, 2002 at 17:56 UTC

    OK, here is my take (using the latest CPAN version of XML::Twig). The only difficult thing here is how to match elements which do _not_ have the vob attribute. I have to look at the XPath spec (Matt?) to figure out how to express it... and then add it to the module. In the meantime I use a coderef to get it (to compute @vob_not I use an anonymous sub that returns true if the element does not have a vob attribute).

    #!/bin/perl -w use strict; use XML::Twig; my $twig= new XML::Twig; $twig->parse( \*DATA); # build the twig my $root= $twig->root; # get the root of the twig (stats) my @vob_not = $root->children( sub { my $elt= shift; !$elt->att( 'v +ob')}); my @vob_none = $root->children( q{player[@vob="none"]} ); my @vob_letter = $root->children( q{player[@vob=~/^[a-z]$/]}); @vob_letter = sort { $a->att( 'vob') cmp $b->att( 'vob') } @vob_letter; my %letters= map { $_->att( 'vob') => 1 } @vob_letter; print "No attribute or vob=none\n", "-" x 20, "\n"; foreach my $player (@vob_not) { printf( "%-20s\n", $player->field( 'name')); } foreach my $player (@vob_none) { printf( "%-20s....none\n", $player->field( 'name')); } print "\n", join( ', ', sort keys %letters), "\n", "-" x 20, "\n"; foreach my $player (@vob_letter) { printf( "%-20s....%1s\n", $player->field( 'name'), $player->att( ' +vob')); } __DATA__ <stats><player vob="d" ><name>Houston, Allan</name><g>69</g></player> <player vob="z" ><name>Sprewell, Latrell</name><g>69</g></player> <player vob="a" ><name>Ewing, Patrick</name><g>49</g></player> <player vob="none" ><name >Johnson, Larry</name><g>57</g></player> <player vob="g" ><name>Camby, Marcus</name><g>48</g></player> <player vob="d" ><name>Thomas, Kurt</name><g>67</g><ppg>7.9</ppg></pla +yer> <player vob="none" ><name >Ward, Charlie</name><g>59</g><ppg>7.5</ppg> +</player> <player vob="none" ><name>Wallace, John</name><g>55</g><ppg>6.6</ppg>< +/player> <player><name>Childs, Chris</name><g>61</g><ppg>5.3</ppg></player> <player><name>Duncan, Tim</name><g>66</g><ppg>23.1</ppg></player> <player><name>Robinson, David</name><g>68</g><ppg>17.1</ppg></player> </stats>

    Update: note that this is not really the most efficient way, XML::Twig will go through the entire list of children for each list it builds), just the easiest to write.

      Thanks Mirod,
      I'm going to give this a try in my program.
      
      I may have over simplified since my clearcase vobs look
      more like BuildEnv, Tools, Dialog, Main_Execs, Misc, Database
      instead of single letters.  I could change the =~ [a-z] to @vob!~/none/
       
      
      
        I could change the =~ [a-z] to @vob!~/none/

        Actually this would not work as the condition gets interpreted by the module, which only groks =~. It will in the next version though ;--)

      Do I need version 3?
      
      I attempted to incorporate the example into my code, and it 
      didn't return anything so I extracted the example exactly as presented, (less pluses), and it didn't return any data.
      
      H:\>mirod_srt.pl
      No attribute or vob=none
      --------------------
      
      
      --------------------
      
      H:\>
      
      
      I reloaded from activestate and checked my version of twig and it is 202.
      
      H:\>perl -v
      
      This is perl, v5.6.1 built for MSWin32-x86-multi-thread
      (with 1 registered patch, see perl -V for more detail)
      
      Copyright 1987-2001, Larry Wall
      
      Binary build 631 provided by ActiveState Tool Corp. http://www.ActiveState.com
      Built 17:16:22 Jan  2 2002
      
      
      Perl may be copied only under the terms of either the Artistic License or the
      GNU General Public License, which may be found in the Perl 5 source kit.
      
      Complete documentation for Perl, including FAQ lists, should be found on
      this system using `man perl' or `perldoc perl'.  If you have access to the
      Internet, point your browser at http://www.perl.com/, the Perl Home Page.
      
      H:\>grep "# XML::Twig" c:\perl\site\lib\xml\twig\twig.pm
      # XML::Twig 2.02 Twig.pm.slow - 2001/01/16
      
      
      Thanks oh great one,
      your humble servant,
      Patrick
      
Re: XML::Twig -- sorting by attribute
by ferrency (Deacon) on Jan 31, 2002 at 17:39 UTC
    Is your main problem an issue with how to do what you want with XML::Twig, or more generally, how to sort on multiple fields in Perl gracefully?

    It looks like you're getting a list of players with XML::Twig easily enough. Once you have that, sorting on two separate fields is not that difficult. Just use two tests in your sort codeblock:

    # Warning, untested. # Given @players as above, my @sorted= sort { $a->atts->{vob} <=> $b->atts->{vob} || $b->first_child_text($field) <=> $a->first_child_text($field) } @players;
    You might need to tweak the attribute test to do the Right Thing for null values. But this is the general technique for sorting on multiple keys: test to see if the primary key is different; if so order on that, and if not, order on the secondary key.

    I hope this is what you were looking for :)

    Alan