http://qs321.pair.com?node_id=184288
Category: Text Processing
Author/Contact Info ignatz
Description: Takes in a LOH (List of Hashes) and an array of keys to sort by and returns a new, sorted LOH. This module closely relates to Sort::Fields. in terms of it's interface and how it does things. One of it's main differences is that it is OO, so one can create a Sort::LOH object and perform multiple sorts on it.

Comments and hash criticism are most welcome. I tried to find something here or on CPAN that did this, but the closest that I got was Sort::Fields. Close, but no cigar. Perhaps there is some simple way to do this with a one liner. Even so, it was fun and educational to write.

package Sort::LOH;

use strict;
use Carp;

use vars qw($VERSION);
$VERSION = '0.01';

######################################################################
+#####
# D O C U M E N T A T I O N

=head1 NAME

Sort::LOH - Sorter for List of Hashes

=head1 SYNOPSIS

    use strict;
    use Sort::LOH;
    my @SAMPLE_DATA = (
        {F1 => "1", F2 => "2",  F3 => "3",   
            FLOAT => "2",    ST => "123 Main Street"},
        {F1 => "2", F2 => "3",  F3 => "4",   
            FLOAT => "9",    ST => "45 Main Street",},
        {F1 => "3", F2 => "4",  F3 => "4",   
            FLOAT => "045",  ST => "2459 Main St"},
        {F1 => "4", F2 => "5",  F3 => "6",   
            FLOAT => "1.3",  ST => "2580 Main Street"},
        {F1 => "5", F2 => "6",  F3 => "7",   
            FLOAT => "9",    ST => "39 Main Street"},
        {F1 => "6", F2 => "7",  F3 => "8",   
            FLOAT => "8.8",  ST => "1888 Main Street"}
    );

    my $sorter = Sort::LOH->new(\@LOH);
    my @sorted = $sorter->sortMe(["F3", "ST"]);

Sorting in reverse order:
    
    my @sorted = $sorter->sortMe(["-F3", "-ST"]);
    
Sorting numerically, as opposed to the default alphabetical:

    my @sorted = $sorter->sortMe(["FLOAT n"]);


=head1 DESCRIPTION

Takes in a LOH (List of Hashes) and an array of keys to sort
by and returns a new, sorted LOH. This module closely relates
to Sort::Fields in terms of it's interface how it does things.
On of it's main differences is that it is OO, so one can create
a Sort::LOH object and perform multiple sorts on it.

=cut

=head1 PUBLIC METHODS

=cut

# D O C U M E N T A T I O N
######################################################################
+#####

######################################################################
+#####
# C O N S T R U C T O R

=head2 new(\@LOH_to_sort) 

The class constructor. To create a Sort::LOH object, simply call:
    
    my $sorter = Sort::LOH->new(\@LOH);

=cut

sub new
{
    my $class = shift;
    my $self  = {};
    bless $self, $class;
    
    unless (ref($self->{LOH} = shift) eq 'ARRAY') {
        croak 'LOH needs a reference to a List of Hashes';
    }
    
    $self->{SORT_BY} = undef;
    return $self;
}

# C O N S T R U C T O R
######################################################################
+#####

######################################################################
+#####
# S T A T I C   M E T H O D S

=head2 element_class() 

The name of the class for use in calling methods. This is a trick to 
simplify inheritance of static factory methods that I got from Perlmon
+ks:
http://www.perlmonks.org/index.pl?node_id=74924 Inheriting classes wou
+ld 
create override element_class with the name of their class.

=cut

sub element_class
{
    return "Sort::LOH";
}

=head2 static(@sort_by, \@LOH_to_sort)

A static method that allows caller to make the class do all the work 
with one swell foop:

    my @sorted = Sort::LOH->static(["F1", "F2"], \@SAMPLE_DATA);

as opposed to
    
    my $sorter = Sort::LOH->new(\@LOH);
    my @sorted = $sorter->sortMe(["F1", "F2"]);

If caller wants to do multiple sorts, one should use the constructor, 
+and
create a Sort::LOH object, since then one only has to pass in the data
+ once:

    my $sorter  = Sort::LOH->new(\@LOH);
    my @sorted  = $sorter->sortMe(["F1", "F2"]); 
    my @revSort = $sorter->sortMe(["-F1", "-F2"]); 

=cut

sub static
{
    my $self    = shift;
    my @sortby  = shift || croak 'USAGE: Sort::LOH->factory(@LIST, \@L
+OH)';
    my @loh     = shift || croak 'Sort::LOH::factory() needs 2 args';

    my $sorter = $self->element_class()->new(@loh); 
    return $sorter->sortMe(@sortby);
}

# S T A T I C   M E T H O D S
######################################################################
+#####

######################################################################
+#####
# C L A S S   M E T H O D S

=head2 sortMe(@sort_by)

The workhorse of this class. Expects a list of the LOH keys to determi
+ne
the sort order for the returned LOH. 

    my @sorted = $sorter->sortMe(["F1", "F2"]);

It is possible to do a reverse sort for a particular key by placing a 
+minus 
sign at the front of it:

    my @sorted = $sorter->sortMe(["-F1", "-F2"]);

If one wants to do a numeric sort, instead of a alphabetical sort, pla
+ce 
" n" after the key in the list:

    my @sorted = $sorter->sortMe(["F1 n", "F2 n"]);

=cut

sub sortMe
{
    my $self  = shift;
    $self->{SORT_BY} = shift || croak 'LOH needs a list of fields to s
+ort by';
    my (@sortcode, @sortedLOH);

    for (@{$self->{SORT_BY}}) {

        unless (/^-?\w+\s*n?$/) {
            croak "improperly formatted sort column specifier '$_'";
        }
        
        # Logic from Sort::Fields
        # Set a and b depending on '-' flag at the start of a key
        my ($a, $b) = /^-/ ? qw(b a) : qw(a b);
        
        # Is it a string or numeric sort?
        my $op = /\s+n$/ ? '<=>' : 'cmp';
        
        # Get the actual column name
        my ($col) = /^-?(\w+)/;

        # Make sure that the sort key being passed in exists.
        if (exists($self->{LOH}[0]{$col})) {
            push @sortcode, "\$${a}->{${col}} $op \$${b}->{${col}}";
        }
    }

    # Croak if there were no valid sort keys specified.
    unless ($sortcode[0]) {
        croak "No valid key match to sort LOH.";
    }
    
    my $sortcode = join " or ", @sortcode;
    $sortcode = "sort { $sortcode } \@{\$self->{LOH}};";

    @sortedLOH = eval "$sortcode";

    if ($@) {
        croak "Sort Failure of LOH\n$@";
    }

    return @sortedLOH;
}

# C L A S S   M E T H O D S
######################################################################
+#####

1;

__END__

=head1 BUGS

=over

=item *

When a LOH is passed in that has a key that isn't present in each
row in the list, and the class is sorted on that key, sort will print
out errors for comparing with undefined values. For instance:

    my @the_data = (
        {ID => "a", CITY => "f1 f"},
        {ID => "b", CITY => "f2 a"},
        {ID => "c"},
        {ID => "f", CITY => "f6 e"}
    );

    my $lohSorter   = Sort::LOH->new(\@the_data);
    my @sorted      = $lohSorter->sortMe(["CITY"]);

Making sure that each key has an empty string as a defined value will 
solve this:

    my @better_data = (
        {ID => "a", CITY => "f1 f"},
        {ID => "b", CITY => "f2 a"},
        {ID => "c", CITY => ""},
        {ID => "f", CITY => "f6 e"}
    );

I haven't figured out a way to trap this error as of yet.

=back

=head1 SUPPORT

The author always welcomes your comments, critiques, suggestions or 
requests.

=head1 AUTHOR

Christopher Baker
<ignatz@ignatzmouse.com>

http://www.ignatzmouse.com

=head1 COPYRIGHT

Copyright (c) 2002 Christopher Baker. All rights reserved.
This program is free software; you can redistribute
it and/or modify it under the same terms as Perl itself.

The full text of the license can be found in the
LICENSE file included with this module.

=head1 SEE ALSO

Sort::File, Data::Table

=cut