How to show data values vertically instead of horizontal way

Viewed 107

I have written a script which will fetch data from db with certain condition and store it in the hash. I need these stored hash content to show in the tabular format. Right now I have all the data which should be shown in the table is there in the hash called %hash.

I need to display the content of the hash in console in tabular format.

Below is my script:

#!/usr/bin/perl

use strict; use warnings;
use Data::Dumper;

my %hash = (
  '2022-08-04' => {
      'Method 1' => {
            'Count' => 50,
            'Size' => '10 MB'
      },
      'Method 2' => {
            'Count' => 40,
            'Size' => '5 MB'
      }
  },
  '2022-08-05' => {
      'Method 1' => {
            'Count' => 30,
            'Size' => '3 MB'
      },
      'Method 2' => {
            'Size' => '50 MB',
            'Count' => '100'
      }
  }
);

my @cols;
my @keys = ('Method 1', 'Method 2');

foreach my $date (sort keys %hash){
    foreach my $method (@keys) {
    push (@recs, { date => $date, size => $hash{$date}{$method}{'Size'}, count => $hash{$date}{$method}{'Count'}});
    }
}

print "cols:\n".Dumper(\@cols)."\n";

my $line1 = '| ' . join(' | ', map { sprintf "%-*s", 20, $_->{date} } @cols) . ' |';
my $line2 = '| ' . join(' | ', map { sprintf "%-*s", 20, $_->{size} } @cols) . ' |';
my $line3 = '| ' . join(' | ', map { sprintf "%-*s", 20, $_->{count} } @cols) . ' |';

print $line1."\n";
print $line2."\n";
print $line3."\n";

Current Result:

| 2022-08-04           | 2022-08-04           | 2022-08-05           | 2022-08-05           |
| 10 MB                | 5 MB                 | 3 MB                 | 50 MB                |
| 50                   | 40                   | 30                   | 100                  |

Expected Result:

-----------------------------------------------------------------------------------
| Date          | Method 1 Size | Method 1 Count | Method 2 Size | Method 2 Count |
-----------------------------------------------------------------------------------
| 2022-08-04    | 10 MB         | 50             | 5 MB          | 40             |
| 2022-08-05    | 3 MB          | 30             | 50 MB         | 100            |
-----------------------------------------------------------------------------------

Since the data I'm pushing into @cols, each of the cols array element contains a hash with date, size and count. And while printing them it shows in horizontal way instead of showing it in vertically.

Also, sometimes there wouldn't be Method 2 hash values. In that scenario I need to display content of Method 1 hash values in table alone. How to achieve it in dynamic way?

3 Answers

You have an outer loop over the fields (the copy and pasted lines), and an inner loop over the flattened records.

But the desired output shows a loop over the dates wrapping a loop over the methods wrapping a loop over the fields.

Structure illustrated

(Click to enlarge to get a better view.)

{
   printf "| %-*s | ", 20, "Date";

   for my $method ( "Method 1", "Method 2" ) {
      printf "%-*s | %-*s |",
         20, "$method Size",
         20, "$method Count",
   }

   printf "\n";
}

for my $date ( sort keys( %hash ) ) {
   my $by_method = $hash{ $date };

   printf "| %-*s | ", $date;

   for my $method ( "Method 1", "Method 2" ) {
      my $rec = $by_method->{ $method };

      printf "%-*s | %-*s |",
         20, $rec->{ size  },
         20, $rec->{ count };
   }

   printf "\n";
}

There's a lot of magic numbers in there.

my @methods = ( "Method 1", "Method 2" );

my $date_size = 20;

my @field_defs = (
   { name => "Size",  size => 20 },
   { name => "Count", size => 20 },
);

{
   printf "| %-*s | ", $date_size, "Date";

   for my $method ( @methods ) {
      for my $field_def ( @field_defs ) {
         printf "%-*s |", $field_def->{ size }, $field_def->{ name };
      }
   }

   printf "\n";
}

for my $date ( sort keys( %hash ) ) {
   my $by_method = $hash{ $date };

   printf "| %-*s | ", $date_size, $date;

   for my $method ( @methods ) {
      my $rec = $by_method->{ $method };

      for my $field_def ( @field_defs ) {
         printf "%-*s |", $field_def->{ size }, $rec->{ $field_def->{ name } };
      }
   }

   printf "\n";
}

The next step might be to calculate the minimum sizes instead of using 20.

When you do programing, it always makes sense to do the steps one after another. If you for example have a complex data-structure and want to create a table from it. It makes sense to transform the data into what best-fit your target. So in this case, create an array of hashes, or just array of array that reprents your data.

Once you have that, you can do all kind of stuff to create a string output as a table from it. But never do both things at once.

So here is another solution.

#!/usr/bin/env perl
use strict;
use warnings;
use v5.32;
use Data::Printer;
use Text::Table;

my %hash = (
  '2022-08-04' => {
      'Method 1' => {
            'Count' => 50,
            'Size' => '10 MB'
      },
      'Method 2' => {
            'Count' => 40,
            'Size' => '5 MB'
      }
  },
  '2022-08-05' => {
      'Method 1' => {
            'Count' => 30,
            'Size' => '3 MB'
      },
      'Method 2' => {
            'Size' => '50 MB',
            'Count' => '100'
      }
  }
);

# First get all the keys of the hashes
my @dates   = keys %hash;
my %methods = map { map { $_ => 1 } keys %$_ } values %hash;
my @methods = keys %methods;
my %args    = map { map { $_ => 1 } keys %$_ } map { values %$_ } values %hash;
my @args    = keys %args;

p @dates;
#[
#    [0] "2022-08-05" (dualvar: 2022),
#    [1] "2022-08-04" (dualvar: 2022)
#]
p @methods;
#[
#    [0] "Method 2",
#    [1] "Method 1"
#]
p @args;
#[
#    [0] "Count",
#    [1] "Size"
#]


# Then create a new data-structure from it
my @data;
for my $date ( @dates ) {
    my %data;
    $data{Date} = $date;
    for my $method ( @methods ) {
        for my $arg ( @args ) {
            $data{$method . ' ' . $arg} = $hash{$date}{$method}{$arg};
        }
    }
    push @data, \%data;
}

p @data;
# [
#    [0] {
#            Date               "2022-08-05" (dualvar: 2022),
#            "Method 1 Count"   30,
#            "Method 1 Size"    "3 MB" (dualvar: 3),
#            "Method 2 Count"   100,
#            "Method 2 Size"    "50 MB" (dualvar: 50)
#        },
#    [1] {
#            Date               "2022-08-04" (dualvar: 2022),
#            "Method 1 Count"   50,
#            "Method 1 Size"    "10 MB" (dualvar: 10),
#            "Method 2 Count"   40,
#            "Method 2 Size"    "5 MB" (dualvar: 5)
#        }
# ]

## Now, you can create your table from it
## I'm using Text::Table

my @fields     = ("Date", "Method 1 Count", "Method 1 Size", "Method 2 Count", "Method 2 Size");
my @table_data = map { [ @$_{@fields} ] } @data;

my $table = Text::Table->new(@fields);
$table->load(@table_data);
print $table;

# Date       Method 1 Count Method 1 Size Method 2 Count Method 2 Size
# 2022-08-05 30             3 MB          100            50 MB
# 2022-08-04 50             10 MB          40            5 MB

If you don't use something like Text::Table you can still print a table on your own.

So the last bit of code changes to.

my @fields     = ("Date", "Method 1 Count", "Method 1 Size", "Method 2 Count", "Method 2 Size");
my @table_data = map { [ @$_{@fields} ] } @data;

# Get the maximum length of every field
my @lengths = (0) x @fields;
for my $row ( \@fields, @table_data ) {
    for (my $idx=0; $idx < @fields; $idx++) {
        $lengths[$idx] = max($lengths[$idx], length $row->[$idx]);
    }
}

# Print the Table
print_with_length(\@fields, \@lengths, " | ");
for my $row ( @table_data ) {
    print_with_length($row, \@lengths, " | ");
}


sub max {
    my ($x, $y) = @_;
    return $x > $y ? $x : $y;
}

sub print_with_length {
    my ($array, $lengths, $seperator) = @_;
    my $sep = $seperator // " ";
    for (my $idx = 0; $idx < @$array; $idx++) {
        my $l = $lengths->[$idx] // 5;
        printf "%${l}s$sep", $array->[$idx];
    }
    print "\n";
}

It prints a table like:

      Date | Method 1 Count | Method 1 Size | Method 2 Count | Method 2 Size | 
2022-08-04 |             50 |         10 MB |             40 |          5 MB | 
2022-08-05 |             30 |          3 MB |            100 |         50 MB | 

Added as a new answer, because i think both styles are worth. Like i said in my other answer. Do the transformation of data before-hand. Then create your table. Here is another way of data-transformation by flattening the inner-hash first.

#!/usr/bin/env perl
use strict;
use warnings;
use v5.32;
use Data::Printer;
use Text::Table;
use List::Util qw(uniqstr);

my %hash = (
  '2022-08-04' => {
      'Method 1' => {
            'Count' => 50,
            'Size' => '10 MB'
      },
      'Method 2' => {
            'Count' => 40,
            'Size' => '5 MB'
      }
  },
  '2022-08-05' => {
      'Method 1' => {
            'Count' => 30,
            'Size' => '3 MB'
      },
      'Method 2' => {
            'Size' => '50 MB',
            'Count' => '100'
      }
  }
);

# Transform %hash by flattening the inner hashes
my %data;
for my $date ( keys %hash ) {
    $data{$date} = hashref_flatten(" ", $hash{$date});
}

p %data;
# {
#    2022-08-04   {
#        "Method 1 Count"   50,
#        "Method 1 Size"    "10 MB" (dualvar: 10),
#        "Method 2 Count"   40,
#        "Method 2 Size"    "5 MB" (dualvar: 5)
#    },
#    2022-08-05   {
#        "Method 1 Count"   30,
#        "Method 1 Size"    "3 MB" (dualvar: 3),
#        "Method 2 Count"   100,
#        "Method 2 Size"    "50 MB" (dualvar: 50)
#    }
# }

# Get all date and fields sorted
my @fields     = sort {$a cmp $b} uniqstr map { keys %$_ } values %data;
my @dates      = sort keys %data;
my @table_data = map {[ $_, hashref_fields($data{$_}, @fields) ]} @dates;

## Now, you can create your table from it
## I'm using Text::Table

my $table = Text::Table->new("Date", @fields);
$table->load(@table_data);
print $table;

# Date       Method 1 Count Method 1 Size Method 2 Count Method 2 Size
# 2022-08-04 50             10 MB          40            5 MB
# 2022-08-05 30             3 MB          100            50 MB



## Helper Functions

# Turns a Hash of Hash (HoH) into a single hash by combining the keys
sub hashref_flatten {
    my ( $sep, $hash ) = @_;
    my %nk;
    while ( my ($key, $value) = each %$hash ) {
        for my $inner_key ( keys %$value ) {
            $nk{$key.$sep.$inner_key} = $hash->{$key}{$inner_key};
        }
    }
    return wantarray ? %nk : \%nk;
}

# Returns multiple fields of a hashref
# can be inlined, but is hard to read/understand
sub hashref_fields {
    my ($hash, @fields) = @_;
    return @$hash{@fields};
}

Related