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

I have to hack some reports together with running totals. I was wondering if there was some efficient way of accumulating fields in an upward-compatible manner -- that is, should I have to add another column I have to change as little code as possible.

I was wondering if I could coax += into being distributive across a hash slice, so I could do the following:

#! /usr/bin/perl -w use strict; my( %totals, %delta ); my @fields = qw/foo bar/; @totals{ @fields } = ( 10, 20 ); @delta{ @fields } = ( 5, 6 ); # the wish # @totals{ @fields } += @delta{ @fields }; # the reality $totals{$_} += $delta{$_} foreach( @fields ); use Data::Dumper; print Dumper( \%totals );

Of course in both cases the code is not dependent on the exact fields used, so the main goal has been achieved, but I find the reality code a little ugly. I was wondering if there was some other elegant method of doing this that escapes me. The fields are already in a hash, as I am using DBI's fetchrow_hashref.


--
g r i n d e r

Replies are listed 'Best First'.
Re: Using += in array context
by Abigail (Deacon) on Jun 05, 2001 at 20:11 UTC
    #!/opt/perl/bin/perl -w use strict; use Tie::Hash; package Hash::Add; @Hash::Add::ISA = qw /Tie::StdHash/; use overload '+' => sub { my ($self, $other) = @_; foreach my $key (keys %$self) { $self -> {$key} += $other -> {$key}; } }; package main; my $totals = tie my %totals => 'Hash::Add'; my $delta = tie my %delta => 'Hash::Add'; my @fields = qw /foo bar/; @totals {@fields} = (10, 20); @delta {@fields} = (5, 6); $totals += $delta; print "Foo = $totals{foo}\n"; print "Bar = $totals{bar}\n"; __END__ Foo = 15 Bar = 26

    -- Abigail

      Good idea! of course, this doesn't work if you only want to add up some of the fields in the hash. It sums up all the fields in the receiver. Perhaps this modification to abigail's code would be an improvement, since it allows you to specify the desired fields:

      #!/opt/perl/bin/perl -w use strict; use Data::Dumper; use Tie::Hash; package Hash::Add; @Hash::Add::ISA = qw /Tie::StdHash/; sub sumFields { my $self = shift; $self->{__summedFields} = [ @_ ]; } sub TIEHASH { my $classname = shift; my $self = $classname->SUPER::TIEHASH; $self->sumFields(@_); $self; } use overload '+' => sub { my ($self, $other) = @_; my @keys = @{$self->{__summedFields}}; @keys = keys(%$self) if (!@keys); foreach my $key (@keys) { next if $key eq '__summedFields'; $self -> {$key} += $other -> {$key}; } }; package main; my @fields = qw /foo bar/; my $totals = tie my %totals => 'Hash::Add', @fields; my $delta = tie my %delta => 'Tie::StdHash'; @totals{ 'a', 'z' } = ( 123, 456 ); @delta{ 'a', 'z' } = ( 789, 234 ); @totals {@fields} = (10, 20); @delta {@fields} = (5, 6); $totals += $delta; print "Foo = $totals{foo}\n"; print "Bar = $totals{bar}\n"; __END__ Foo = 15 Bar = 26
      update: set fields a and z in right place
Re: Using += in array context
by Spudnuts (Pilgrim) on Jun 05, 2001 at 18:18 UTC
    Would it work to make fuller use of your SQL server?
    SELECT SUM(col1) col1_sum, SUM(col2) col2_sum FROM table
Re: Using += in array context
by jeroenes (Priest) on Jun 05, 2001 at 18:18 UTC
    At first, I was tempted to overload +=, but than I realised that was not going to work, as hashes don't have any garanteed order.

    So you really have to stick with the code you already have. Hmm. You always can write a sub, which names the function out loud:

    sub add_hash_slice{ my $slice = shift or return undef; my $hash1 = shift or return undef; my $hash2 = shift or return $hash1; $hash1->{$key} += $hash2->{$key} for my $key( @$slice ); return $hash1; } #call add_hash_slice( \@fields, \%totals, \%delta);
    Alternativeley, if you have only values in the 'table', you can load it into PDL.
    use PDL; my $a = pdl $AoA; $total1=sum($a->(':,0'));
    Hope this helps

    Jeroen
    "We are not alone"(FZ)

Re: Using += in array context
by Henri Icarus (Beadle) on Jun 05, 2001 at 17:53 UTC

      Personally, I'd use the foreach code from the original question. I find it clearer and cleaner than even the "wish" code. For one thing, "@fields" doesn't have to be repeated in that code.

      If grinder finds     $totals{$_} += $delta{$_} foreach( @fields ); "ugly", then I doubt

      use mapcar; mapcar { $_[0] += $_[1] } [@totals{@fields}], [@delta{@fields}];
      would fair any better.

              - tye (but my friends call me "Tye")
Re: Using += in array context
by toma (Vicar) on Jun 05, 2001 at 20:32 UTC
    For maintainability I really like Data::Table, since it allows me to work directly with column names in my table. It can also work with column numbers. It uses SQL or CSV as a data source. It also handles sorting, etc.
    use Data::Table; my $sum=0; # my $t= Data::Table::fromSQL ($dbh, $sql, $vars); # Database versio +n my $t= Data::Table::fromCSV("data.csv"); $sum += $_ for($t->col('exp')); print $sum,"\n";
    It should work perfectly the first time! - toma
Re: Using += in array context
by bikeNomad (Priest) on Jun 05, 2001 at 19:58 UTC
    This is probably too simple-minded, but:

    #! /usr/bin/perl -w use strict; use Data::Dumper; my( %totals, %delta ); my @fields = qw/foo bar/; @totals{ @fields } = ( 10, 20 ); @delta{ @fields } = ( 5, 6 ); sub sumup(\@\%\%) { $_[1]->{$_} += $_[2]->{$_} foreach (@{$_[0]}); } sumup(@fields, %totals, %delta); print Dumper( \%totals );
Re: Using += in array context
by mattr (Curate) on Jun 06, 2001 at 11:15 UTC
    How about this? You can then do some other wild matrix algebra things besides just addition.
    #!/usr/bin/perl use strict; package ary; use Math::Matrix; @ary::ISA = qw(Math::Matrix); use overload '+' => \&aryadd; sub aryadd { return $_[0]->add($_[1]); }; package main; my( %totals, %delta ); my @fields = qw/foo bar bat/; @totals{ @fields } = ( 10, 20, 30 ); @delta{ @fields } = ( 5, 6, 1 ); my $t = new ary([@totals{ @fields }]); my $d = new ary([@delta{ @fields }]); $t += $d; # this works too: # $t = $t->add($d); # $t = $t + $d; $t->print("Totals:\n"); # this works too: # my $aref = $t->[0]; print "> $_\n" foreach (@$aref);

    Updated: Spurred on to create this version which implements overloading. If you just use Math::Matrix objects and their add method as-is, you don't need an extra package but it's less maintainable and less fun. :)

    Updated again: Added last two lines to pull data out of Matrix object.