Beefy Boxes and Bandwidth Generously Provided by pair Networks
The stupid question is the question not asked

Text::Table Tutorial

by biohisham (Priest)
on Jun 26, 2010 at 12:26 UTC ( #846667=perltutorial: print w/replies, xml ) Need Help??

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 .

Log In?

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perltutorial [id://846667]
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others scrutinizing the Monastery: (3)
As of 2023-12-09 09:44 GMT
Find Nodes?
    Voting Booth?
    What's your preferred 'use VERSION' for new CPAN modules in 2023?

    Results (37 votes). Check out past polls.