We're routinely facing situations where we need to quickly master to a functional competency level a particular module usage. What can greatly influence
the depth of the learning curve and the retention of information afterwards is the module documentation; exceedingly technical documentation is dry and
maybe discouraging to revisit over and over, sparse documentation doesn't do justice to the module capabilities and leaves much unexplained, lack of code
illustrations makes it uneasy for the example-oriented readers. The size of the module too plays a role on how efficient and clear it is documented, however
there are some very large modules with very great documentation and support. Much kudos go to all the Perl community for all the selfless efforts and to
the Monastery for providing a place to learn more and add more to the community in ways that compensate for these caveats and say thank you to all the
whole entire Perl initiative.
In Perl there exists many different approaches to handling textual input and then (re)formatting the output to assume an intelligible design
(or a mysteriously cabalistic one!). Perl text processing capabilities are well established, this is reflected in many areas like
Regular Expressions,Predefined Variables,
Functions and Operators such as cmp, eq, uc,lc, pack,unpack...etc, and the existence of many CPAN
categories and namespaces to modules that take text processing and output formatting
capabilities to skylimits.
Unarguably, tabulated representation of data has significant values, cluttered or clumsy reports can make for grumby colleagues, a lashing boss,
and a ruined day (if not career), Perl springs to the rescue, programming interfaces libraries to enable carrying out of such represntation jobs
easier are abundantly spread across many CPAN namespaces too; Alzabo::Table,
Table, PDF::Table,
Tk::Table,Spreadsheet::WriteExcel
and Text::Table are all but few examples of libraries to handle
tabulation tasks with efficiency.
Here I am attempting to provide a quick jumpstart to the module Text::Table
This tutorial in no way does attempt to replace the original documentation to the module rather than to add to it.
The best thing about this module is that it allows you to generate flexible tables with dynamic column alignment attributes, that is, the column width is
dictated by the size of the widest item it has, and data items within the column can be aligned to the left, right or center from the column boundary. This
lifts off from the burden a bit of the trial and re-trial of different spacing arrangements via sprintf for example.
What you need before we get started is a general knowledge level regarding OOP, Referencing and Dereferencing and the module
Text::Table installed, check Installing Modules if you are not familiar with installing Perl non-core modules, then you need a dataset to
play with while wading along, this is brought to you from the Martian Blossoms Juniors saved in Kindergarten.txt
Acklay
10
genetics
2051
Bith
12
anthropology
2053
Blistmok
14
metaphysics
2051
Chiilak
17
2051
Lylek
2053
Massiff
13
botany
2053
Reading this dataset into two hashes of anonymous hashes,
%students_51 and
%students_53 each with the student names as keys whose values are
anonymous hashes with the keys (name, age, topic, class). ALL these keys are your variables (table headers) while their values are data for your table.
Each table represents a class year, 2051 or 2053. Note the
Data::Dumper output has empty values for some of the students at some of
the variables since the parsing code below reads a line per line through the dataset and whereof blanks are taken to correspond to empty values for the
corresponding variables,
#!/usr/local/bin/perl
use strict;
use warnings;
use Data::Dumper;
my $file = "C:/Documents and Settings/franciscor/Desktop/Kindergarten.
+txt";
open my $fh, '<', $file or die("Error opening file $!");
my %students_51;
my %students_53;
until ( eof($fh) ) {
chomp( my $name = <$fh> );
chomp( my $age = <$fh> );
chomp( my $topic = <$fh> );
chomp( my $class = <$fh> );
if ( $class == 2051 ) {
$students_51{$name}->{'age'} = $age;
$students_51{$name}->{'topic'} = $topic;
$students_51{$name}->{'class'} = $class;
}
elsif ( $class == 2053 ) {
$students_53{$name}->{'age'} = $age;
$students_53{$name}->{'topic'} = $topic;
$students_53{$name}->{'class'} = $class;
}
}
close $fh;
print Dumper(\%students_51,\%students_53);
#OUTPUT:
$VAR1 = {
'Chiilak' => {
'topic' => '',
'class' => 2051,
'age' => '17'
},
'Blistmok' => {
'topic' => 'metaphysics',
'class' => 2051,
'age' => '14'
},
'Acklay' => {
'topic' => 'genetics',
'class' => 2051,
'age' => '10'
}
};
$VAR2 = {
'Lylek' => {
'topic' => '',
'class' => 2053,
'age' => ''
},
'Massiff' => {
'topic' => 'botany',
'class' => 2053,
'age' => '13'
},
'Bith' => {
'topic' => 'anthropology',
'class' => 2053,
'age' => '12'
}
};
Now, lets create tables for each of our two classes, later we will merge these tables together. Creating tables involves creating a table object of the module
Text::Table foreach student from
Martian Blossoms Juniors and this entails (in part) looping through the hashes
%students_51 and
students_53. Creating a table object can also allow you to create table column-heads on the go:
use Text::Table;
my $table_51 = Text::Table->new("NAME","AGE","TOPIC","CLASS");
Text::Table adds data to the table in line-wise or in bulk fashions, there are two functions achieving this, the
add() and
load(),
with the later being the bulk loader of a group of datalines to the table at once.
Interestingly,
Text::Table performance doesn't crumble on empty values.
foreach my $key(keys %students_51){
#Direct interpolation to the add function of Text::Table
$table_51->add(
$key,
"$students_51{$key}->{'age'}",
"$students_51{$key}->{topic}",
"$students_51{$key}->{class}"
); # a record of data
}
$table_51->add(' '); #ADD AN EMPTY Record
print $table_51;
#OUTPUT:
NAME AGE TOPIC CLASS
Chiilak 17 2051
Blistmok 14 metaphysics 2051
Acklay 10 genetics 2051
Every call to
add() inserts a line of values into the table, this line corresponds to a table record, a white-space fed to either
add() or
load() is an empty table record, this comes in handy if you're
manually merging tables together as a measure to ensure that they have the
same number of lines (records).
Text::Table has taken care of formatting and aligning its own cells in columns efficienty, since
Chiilak
didn't have any info filled in the
topic section for her entry that cell was left blank.
Let's add data in bulk to the table table_53 this is simply achievable by passing an array reference or an anonymous array to load(),
the headers to this table are somewhat fancy that they are underlined. A fourth student named Zuxu whose incomplete information record is provided in an
array named @zuxu_data is added simply by passing a reference to @zuxu_data from within load() sufficing its requirement and adding
that record to the table in return.
#Zuxu record
my @Zuxu_data = ("Zuxo"," "," ","2053"); #blanks serve as placeholders
+ here
my $table_53 = Text::Table->new( "NAME\n-----", "AGE\n---", "TOPIC\n--
+---", "CLASS\n-----" );
my @Zuxu_data = ( "Zuxo", " ", " ", "2053" );
$table_53->load(
[
"Bith", "$students_53{Bith}->{'age'}"
+,
"$students_53{Bith}->{'topic'}", "$students_53{Bith}->{'class'
+}"
],
[
"Lylek", "$students_53{Lylek}->{'age'
+}",
"$students_53{Lylek}->{'topic'}", "$students_53{Lylek}->{'clas
+s'}"
],
[
"Massiff",
"$students_53{Massiff}->{'age'}",
"$students_53{Massiff}->{'topic'}",
"$students_53{Massiff}->{'class'}"
],
\@Zuxu_data
);
print $table_53;
#OUTPUT:
NAME AGE TOPIC CLASS
----- --- ----- -----
Bith 12 anthropology 2053
Lylek 2053
Massiff 13 botany 2053
Zuxo 2053
Each record in the table is a standalone data line that can be accessed/retrieved individually, also any cell that has a new line character would be taken
as a two-line cell, so here, our header for the table $table_53 has in fact two lines. Passing the line number to the function table()
would return that line (or those group of lines as in the following example), did we need \n in the print statement inside the
foreach loop? Why (not)?.
#print the header for $table_53 and Massiff's record.
foreach(0,1,4){
print $table_53->table($_);
}
#OUTPUT:
NAME AGE TOPIC CLASS
----- --- ----- -----
Massiff 13 botany 2053
Another variation to
table() allows you to print a selection of lines together starting from a given table line:
#show me 3 records starting from line 2
print $table_53->table(2,3);
#OUTPUT:
Bith 12 anthropology 2053
Lylek 2053
Massiff 13 botany 2053
If you really cared less about the entire table but more about its body, then invoking the more becoming function
body() is more direct forward and
it can be used in essentially the same way
table() can.
Another approach to invoking the above two functions is without passing a subscript as an argument, this way you can assign the entire table to an array with
or without its headers. The table data alignment is uniformly intact as well
my @headed_table = $table_53->table();
my @headless_table = $table_53->body();
print @headless_table;
print "+" x 50, "\n";
print @headed_table;
#OUTPUT:
Bith 12 anthropology 2053
Lylek 2053
Massiff 13 botany 2053
Zuxo 2053
++++++++++++++++++++++++++++++++++++++++++++++++++
NAME AGE TOPIC CLASS
----- --- ----- -----
Bith 12 anthropology 2053
Lylek 2053
Massiff 13 botany 2053
Zuxo 2053
or assign a record thereof to a variable and the alignment and spacing is maintained too.
print $table_53->body();
print "\n\n";
my $third_record = $table_53->body(2);
print $third_record;
#OUTPUT:
Bith 12 anthropology 2053 <---The entire table body
Lylek 2053
Massiff 13 botany 2053
Zuxo 2053
Massiff 13 botany 2053 <----Third Record only
Check the function
select() from
Text::Table
Let's piece the code we have so far together:
#!/usr/local/bin/perl
use strict;
use warnings;
use Data::Dumper;
my $file = "C:/Documents and Settings/franciscor/Desktop/Kindergarten.
+txt";
open my $fh, '<', $file or die("Error opening file $!");
my %students_51;
my %students_53;
until ( eof($fh) ) {
chomp( my $name = <$fh> );
chomp( my $age = <$fh> );
chomp( my $topic = <$fh> );
chomp( my $class = <$fh> );
if ( $class == 2051 ) {
$students_51{$name}->{'age'} = $age;
$students_51{$name}->{'topic'} = $topic;
$students_51{$name}->{'class'} = $class;
}
elsif ( $class == 2053 ) {
$students_53{$name}->{'age'} = $age;
$students_53{$name}->{'topic'} = $topic;
$students_53{$name}->{'class'} = $class;
}
}
close $fh;
print Dumper(\%students_51,\%students_53);
use Text::Table;
my $table_51 =
Text::Table->new( "NAME\n-----", "AGE\n---", "TOPIC\n-----", "CLASS\
+n-----" );
foreach my $key ( keys %students_51 ) {
#Direct interpolation to the add function of Text::Table
$table_51->add( $key, "$students_51{$key}->{'age'}",
"$students_51{$key}->{topic}", "$students_51{$key}->{class}" )
+;
}
$table_51->add(' '); #ADD AN EMPTY LINE
print $table_51;
#since we are using a bulk loader we don't need to loop through %stude
+nts_53
#Of course this is inefficient if your table has many rows and columns
my $table_53 =
Text::Table->new( "NAME\n-----", "AGE\n---", "TOPIC\n-----", "CLASS\
+n-----" );
my @Zuxu_data = ( "Zuxo", " ", " ", "2053" );
$table_53->load(
[
"Bith", "$students_53{Bith}->{'age'}"
+,
"$students_53{Bith}->{'topic'}", "$students_53{Bith}->{'class'
+}"
],
[
"Lylek", "$students_53{Lylek}->{'age'
+}",
"$students_53{Lylek}->{'topic'}", "$students_53{Lylek}->{'clas
+s'}"
],
[
"Massiff",
"$students_53{Massiff}->{'age'}",
"$students_53{Massiff}->{'topic'}",
"$students_53{Massiff}->{'class'}"
],
\@Zuxu_data
);
print $table_53;
If we loaded our two tables bodies into the arrays @headless_51 and @headless_53 respectively and attempted to conceptually merge them
-side by side- we will not get the merging that we want since each table record is a standlone line.
my @headless_51 = $table_51->body();
my @headless_53 = $table_53->body();
print "@headless_51 @headless_53";
#OUTPUT:
Chiilak 17 2051
Blistmok 14 metaphysics 2051
Acklay 10 genetics 2051
Bith 12 anthropology 2053
Lylek 2053
Massiff 13 botany 2053
Zuxo 2053
As you've noticed all the indices for these functions from
Text::Table that accept indices for their arguements are zero based,
select() is
not an exception too. It allows you to quickly generate subtables from your tables and merge them in the ordering you want simply by passing column numbers
from the parent tables to be selected into the subtable, It is also possible to rearrange the columns in the subtable in the ordering that you want or make
a column appear more than once just by passing to
select() that column index more than once!.
Let's use the
select() function to quickly generate 6 subtables from
table_51 and
table_53 and merge them together.
This is just to show you the possibilities out there, however, you can extend on it and provide measures for skipping empty records and all that jazz.
my $merged_tables = Text::Table->new("\tTABLE 51 ","\tTABLE 53");
$merged_tables->add($table_51->select(0,1,2,3),$table_53->select(3,2,1
+,0));
print $merged_tables;
#OUTPUT:
TABLE 51 TABLE 53
NAME AGE TOPIC CLASS CLASS TOPIC AGE NAME
----- --- ----- ----- ----- ----- --- -----
Chiilak 17 2051 2053 anthropology 12 Bith
Blistmok 14 metaphysics 2051 2053 Lylek
Acklay 10 genetics 2051 2053 botany 13 Massiff
2053 Zuxo
my $merged_name_age=Text::Table->new("\tAGES OF CLASSES","");
$merged_name_age->add($table_51->select(0,1),$table_53->select(0,1));
print $merged_name_age;
#OUTPUT:
AGES OF CLASSES
NAME AGE NAME AGE
----- --- ----- ---
Chiilak 17 Bith 12
Blistmok 14 Lylek
Acklay 10 Massiff 13
Zuxo
my $class_topic=Text::Table->new("Subjects Of Classes","");
$class_topic->add($table_51->select(2,3),$table_53->select(2,3));
print $class_topic;
#OUTPUT:
Subjects Of Classes
TOPIC CLASS TOPIC CLASS
----- ----- ----- -----
2051 anthropology 2053
metaphysics 2051 2053
genetics 2051 botany 2053
2053
If generating your own merging protocol is preferrable you might want to loop through the tables, spliting each record into individual values and conditionally
select matching values that will be added to your new table. It'd prove nightmarish IF the tables don't have the same number of records for that matter hence
you could pad the original tables by adding empty records here and there to ensure that they have equal number of records.
Check the following conditional merge code listing.
# @Dlines = $table1->body();
# @DPlines = $table2->body();
my $alignedTable = Text::Table->new();
for(my $i = 0 .. $#Dlines){
my($D1) = split(/\s{2,}/,$Dlines[$i]) if ($Dlines[$i] !~/^\s*
++$/);
my($D2) = split(/\s{2,}/,$DPlines[$i]) if ($DPlines[$i]!~/^\s*
++$/);;
$alignedTable->load([$D1, $D2]);
}
print $alignedTable;
Thank you and I hope that this tutorial is going to be successful at taking you to tackling bigger and bigger tasks galaxy-wide.
Original RFC published RFC(Tutorial):Text::Table Enable on Jun 14, 2010
Excellence is an Endeavor of Persistence.
A Year-Old Monk :D .