Perl - Splitting a string - arrays

I'm doing an Array that contents each word of a phrase. When I try to split it and print the length then the console gives me an enormous number such as 111039391231319239188238139123919232913123... (more lines)
why?
Here's my code:
$mynames = $texto3;
print $mynames. "\n";
#nameList = split(' ', $texto3);
#print #nameList.length();
for ($to = 0; $to<#nameList.length; $to++){
if($to<#nameList.length) {
#nameList[$to] = #nameList[$to] . "_" . #nameList[$to++];
}
print $to;
#print #nameList[$to] . "\n";
}
$string_level2 = join(' ', #nameList);
#print $string_level2;

To get the length of an array use scalar #nameList instead of #nameList.length.
A typical for-loop uses the less-than operator when counting up, e.g.:
for ( $to = 0; $to < scalar(#nameList); $to++ ) ...
You should never use a post-increment unless you understand the side effects. I believe the following line:
#nameList[$to] = #nameList[$to] . "_" . #nameList[$to++];
... should be written as ...
$nameList[$to] = $nameList[$to] . "_" . $nameList[$to + 1];
Finally the comparison you use should account for the boundary condition (because you refer to $to + 1 inside the loop):
if( $to < (scalar(#nameList) - 1) ) {
$nameList[ $to ] = $nameList[ $to ] . "_" . $nameList[ $to + 1 ];
}

Related

loop through elements of array to find character perl

I have a perl array where I only want to loop through elements 2-8.
The elements are only meant to contain numbers, so if any of those elements contain a letter, I want to set an error flag = 1, as well as some other variables as seen.
The reason I have 2 error flag variables is due to scope rules within the loop.
fields is an array, I created by splitting another irrelevant array by the " " key.
So, when I try to print error_line2, error_fname2 from outside the loop, I get this:
Use of uninitialized value $error_flag2 in numeric eq (==)
I don't know why, because I've initialized the value within the loop and created the variable outside the loop.
Not sure if I'm even looping to find characters correctly, so then it's not setting the error_flag2 = 1.
Example line:
bob hankerman 2039 3232 23 232 645 64x3 324
since element 7 has the letter 'x' , I want the flag to be set to 1.
#!/usr/bin/perl
use strict;
use warnings;
use Scalar::Util qw(looks_like_number);
my $players_file = $ARGV[0];
my #players_array;
open (my $file, "<", "$players_file")
or die "Failed to open file: $!\n";
while(<$file>) {
chomp;
push #players_array, $_;
}
close $file;
#print join "\n", #players_array;
my $num_of_players = #players_array;
my $error_flag;
my $error_line;
my $error_fname;
my $error_lname;
my $error_flag2=1;
my $error_line2;
my $error_fname2;
my $error_lname2;
my $i;
foreach my $player(#players_array){
my #fields = split " ", $player;
my $size2 = #fields;
for($i=2; $i<9; $i++){
print "$fields[$i] \n";
if (grep $_ =~ /^[a-zA-Z]+$/){
my $errorflag2 = 1;
$error_flag2 = $errorflag2;
my $errorline2 = $player +1;
$error_line2 = $errorline2;
my $errorfname2 = $fields[0];
$error_fname2 = $errorfname2;
}
}
if ($size2 == "9" ) {
my $firstname = $fields[0];
my $lastname = $fields[1];
my $batting_average = ($fields[4]+$fields[5]+$fields[6]+$fields[7]) / $fields[3];
my $slugging = ($fields[4]+($fields[5]*2)+($fields[6]*3)+($fields[7]*4)) / $fields[3];
my $on_base_percent = ($fields[4]+$fields[5]+$fields[6]+$fields[7] +$fields[8]) / $fields[2];
print "$firstname ";
print "$lastname ";
print "$batting_average ";
print "$slugging ";
print "$on_base_percent\n ";
}
else {
my $errorflag = 1;
$error_flag = $errorflag;
my $errorline = $player +1;
$error_line = $errorline;
my $errorfname = $fields[0];
$error_fname = $errorfname;
my $errorlname = $fields[1];
$error_lname = $errorlname;
}
}
if ($error_flag == "1"){
print "\n Line $error_line : ";
print "$error_fname, ";
print "$error_lname :";
print "Line contains not enough data.\n";
}
if ($error_flag2 == "1"){
print "\n Line $error_line2 : ";
print "$error_fname2, ";
print "Line contains bad data.\n";
}
OK, so the problem you've got here is that you're thinking of grep in Unix terms - a text based thing. It doesn't work like that in perl - it operates on a list.
Fortunately, this is pretty easy to handle in your case, because you can split your line into words.
Without your source data, this is hopefully a proof of concept:
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
while ( <DATA> ) {
#split the current line on whitespace into an array.
#first two elements get assigned to firstname lastname, and then the rest
#goes into #values
my ( $firstname, $lastname, #values ) = split; #works on $_ implicitly.
#check every element in #values, and test the regex 'non-digit' against it.
my #errors = grep { /\D/ } #values;
#output any matches e.g. things that contained 'non-digits' anywhere.
print Dumper \#errors;
#an array in a scalar context evaluates as the number of elements.
#we need to use "scalar" here because print accepts list arguments.
print "There were ", scalar #errors, " errors\n";
}
__DATA__
bob hankerman 2039 3232 23 232 645 64x3 324
Or reducing down the logic:
#!/usr/bin/perl
use strict;
use warnings;
while ( <DATA> ) {
#note - we don't need to explicity specify 'scalar' here,
#because assigning it to a scalar does that automatically.
#(split) splits the current line, and [2..8] skips the first two.
my $count_of_errors = grep { /\D/ } (split)[2..8];
print $count_of_errors;
}
__DATA__
bob hankerman 2039 3232 23 232 645 64x3 324
First : You don't need to use "GREP", Simply you can match the string with "=~" in perl and you can print matched value with $&.
Second : You should use $_ if and only if there is not other variable used in the loop. There is already $i used in the loop, you can write the loop as :
for my $i (2..9) {
print "$i\n";
}
or
foreach(2..9) {
print "$_\n";
}

Simplify long if-else chain with regex by comparing with array

for (my $j = 0; $j <100000; $j++){
my $outcome = rand();
for (my $k = 0; $k < #cum_sum; $k++){
if ($cum_sum[$k] >= $outcome){
if ($keys[$k] =~ m/\"|\/|\<|\>|\\|\`|\~|\#|\#|\$|\%|\^|\*|[0-9]/) {
print $out "";
}
if ($keys[$k] =~ m/\s/){
print $out " ";
}
elsif ($keys[$k] =~ m/\&/){
print $out " and ";
}
elsif ($keys[$k] =~ m/\!/){
print $out "! ";
}
elsif ($keys[$k] =~ m/\:/){
print $out ": ";
}
elsif ($keys[$k] =~ m/\'/){
print $out "' ";
}
elsif ($keys[$k] =~ m/\./){
print $out ". ";
}
elsif ($keys[$k] =~ m/\;/){
print $out "; ";
}
elsif ($keys[$k] =~ m/\?/){
print $out "? ";
}
elsif ($keys[$k] =~ m/\,/){
print $out ", ";
}
else {
print $out "$keys[$k]";
}
last;
}
}
# print "$outcome\n";
}
I mostly need help with simplifying the long chain of elsif statements I have.
The logic in the outer for loops works.
#keys is an array of two character (digrams) strings.
I am trying to make the elsif statements more efficient by comparing the digrams from #key to an array of the punctuation marks #punctuation = qw(! : " ' ; ? , .)
Then, if the digram does contain one of the punctuation marks in the punctuation array, the digram gets changed to "punctuation_mark " e.g. "a!" -> "! "
The end result would be that I do not use regexes for any of #punctuation.
However, I am not sure on how to implement this change.
Thank you!
In order to simplify your code, it may have sense to use the Tie::RegexpHash CPAN module. The main idea is build a hash with regexpr as keys, so you easily find the related values by matching:
use Tie::RegexpHash;
my $rehash = Tie::RegexpHash->new();
$rehash->add( qr/\s/, " " );
$rehash->add( qr/\&/, " and " );
#...
my $value = $rehash->match( "&" ); # $value <-- " and "
Use alternation, just like in the first regex, but capture the match and use $1 to replace the word
my $re_punct = join '|', map { quotemeta } qw(& ! : ' . ; ? ,); #'
for my $j (0..99_999) {
my $outcome = rand();
for my $k (0..$#cum_sum) {
...
if ($keys[$k] =~ /($re_punct)/) {
if ($1 eq '&') { $keys[$k] = " and " }
else { $keys[$k] = "$1 " }
}
...
}
}
Comments
quotemeta escapes by \ all "ASCII non-"word" characters"
syntax: $#ary is the index of the last element in #ary, just right for looping over array index
for my $i ($beg .. $end) is much clearer than the equivalent C-style for loop†
The presented logic leaves a question: what if both characters are punctuation?
Note on your idea to do it "By Comparing With Array"
You could use List::MoreUtils::first_value, for instance. It would go like
use List::MoreUtils qw(first_value);
my #punc = map { quotemeta } qw(& ! : ' . ; ? ,); #'
foreach my $word (#words) {
if (my $match = first_value { $word =~ /$_/ } #punct) {
$word = $match;
}
}
The first_value (or firstval) returns the first element of #punct for which the block returns true, and undef if none do. The $word aliases the currently processed element of #words and changing it changes the array element; so you get your replacement.
However, you still have to deal with regex and escape (at least some of) punctuation. Thus I see no advantage in going to this trouble; the "straight-up" regex is far clearer in this case.
† Even compiled languages evolve this way. The C++11 introduced the range-based for loop
for (auto var: container) ... // (really, const auto&), or auto&, or auto&&
and a standard reference linked above says
Used as a more readable equivalent to the traditional for loop [...]
In Perl this is how things are done; just use it.
So, looking at it you have two cases:
One where you replace a set of values, with 'the value and a space'. And another where you replace with something different.
So how about creating a lookup table for each, and processing just two regexes:
#!/usr/bin/env perl.
use strict;
use warnings;
use Data::Dumper;
my %replace = (
'"' => "",
'/' => "",
'&' => " and ",
);
my #add_space = ( ',', '?', ';', '.', "'", ':', '!' );
my $search = join ( "|", map { quotemeta } keys %replace );
$search = qr/($search)/;
my $add_space_after = join "|", map {quotemeta} #add_space;
$add_space_after = qr/($add_space_after)/;
while ( <DATA> ) {
s/\s/ /g;
s/$search/$replace{$1}/;
s/$add_space_after/$1 /;
print;
}
__DATA__
Work:Work
cookies&milk;wordhere
why?are;you/so "sad"
This gives us what you want I think, and - hopefully - keeps the code pretty conscise.
Of importance is the quotemeta function here, because it ensures your metachars are escaped before regexing them.
Note - the only one of your examples this doesn't handle is the \s to " ". But that's IMO probably best to write separately for clarity, as obviously youc can't quotemeta it.

how can be split string by dot and comma both

I have a one string which is include ,(comma) and .(dot) .
I want to split both comma and dot . I am confused to split by dot and comma .
I would go with a regex first and then loop the results, something like this:
$html = <<< LOB
this is example of string , test . R.K
this is second example of string , test2 . R.K2
LOB;
preg_match_all('/^.*?,(.*?)\.(.*?)$/im', $html, $matches, PREG_PATTERN_ORDER);
for ($i = 0; $i < count($matches[0]); $i++) {
echo $matches[1][$i] . " " . $matches[2][$i];
}
//test R.K
//test2 R.K2
DEMO

Transform/pivot array in perl

Im stuck writing Perl code which transforms 2d array.
First column of the array is always date
Second column of the array is key that sorts.
Data is located in array "data" and is ordered by date and then key.
My situation should be understandable from the tables under. Unique values from the second column will be selected and later divided into columns header (green table)
It should work with and number of columns or dates/keys.
Structure before
Structure after
My code:
#creates filtered array of all the unique dates and its count
my #date = #{ $data->[0] };
my #filtDate = uniq #date;
my $countFiltDate = scalar #filtDate;
#unique list of keys
my #klic = #{ $data->[1] };
my #filtKlic = uniq #klic;
#orders filtered keys
#filtKlic = sort #filtKlic;
my $countFiltKlic = scalar #filtKlic;
#count of columns
my $columnsCount = scalar #{$data};
#test code - counts how many new number columns to make.
my $columnsCountAfter = ( $columnsCount - 2 ) * $countFiltKlic;
#inserst filtered dates into first column
my $dataGraph;
for ( my $i = 0; $i < $countFiltDate; $i++ ) {
$dataGraph->[0]->[$i] = #filtDate[$i];
}
#biggest loop with number of dates
for ( my $k = 0; $k < $countFiltDate; $k++ ) {
my $l;
my $c;
#columns sount k $i
for ( my $i = 0; $i < $columnsCount - 2; $i++ ) {
#loop for different keys k $j
for ( my $j = 0; $j < $countFiltKlic; $j++ ) {
$l++; #riadok v prvej tabulke
#EVERYTHING after this part is written horibly.
# I'm trying to make it work even
#if key values are missing.
for ( my $m = 0; $m < 5; $m++ ) {
if ( $data->[1]->[ $l - 1 ] eq $filtKlic[$j] ) {
print " [" . $data->[1]->[ ( $l - 1 ) ] . ',' . $filtKlic[$j] . "]";
$dataGraph->[ $l + $c ]->[$k] = $data->[ $i + 2 ]->[ ( ( $k * $countFiltKlic ) + $j ) ];
#print " [".$data->[1]->[$j].','.($filtKlic[($j)])."]-";
print " [" . ( $i + 2 ) . ',' . ( ( $k * $countFiltKlic ) + $j ) . "]-";
print " [" . ( $l + $c ) . ',' . $k . "]<br>";
$m = 5; #just random number... i don't want to get infinite loops during testing
} else {
if ( $m = 5 ) {
$l--;
$c++;
}
$j++;
}
}
}
}
}
my #nameStlpceKlic;
#nameStlpceKlic[0] = "date";
my $o;
for ( my $i = 0; $i < $columnsCount - 2; $i++ ) {
foreach (#filtKlic) {
my $o;
$o++;
#nameStlpceKlic[$o] = #filtKlic[ ( $o - 1 ) ] . $i;
}
}
I have 2 problems.
How to make sure that this will work even if some of the key are missing at some dates.
How to write it properly. My code is too clumsy.
Here is my general approach for solving this kind of problem.
In the second table, you're grouping your data by the date, then displaying the values for number1 and the values for number2. This should give you a hint as to how you want to organise your data structure and what you need to index for printing.
Your current data is (I assume) stored in an array of arrays. I was too lazy to copy the values, so I made my own AoA with made up values. I've sprinkled comments through the code so you can see how I worked on this.
my $arr = [
['date','key','number1','number2'],
['22.12.2013','1a','1a1-34567','1a2-1234567'],
['22.12.2013','2b','2b1-3249871','2b2-4597134'],
['22.12.2013','3c','3c1-1234567',''],
['22.12.2013','4d','4c1-3249871','4c2-4597134'],
['22.13.2013','1a','1a1-34567','1a2-1234567'],
['22.13.2013','2b','','2b2-4597134'],
['22.13.2013','3c','3c1-1234567','3c2-1234567'],
['22.13.2013','4d','4c1-3249871','4c2-4597134'],
];
# first, remove the first row, which contains the column headers.
my $col_h = shift #$arr;
my $data;
my $key_list;
foreach (#$arr) {
my %hash;
# use a hash slice with the column header array as keys
# and the array as the values
#hash{#$col_h} = #$_;
# store this hash in a data hash indexed by date then key
$data->{ $hash{date} }{ $hash{key} } = \%hash;
# compile a separate hash with the keys in it
$key_list->{ $hash{key} }++;
}
# make a sorted list of keys, ready for printing
my #key_list = sort keys %$key_list;
# remove the first two elements from the column headers ('date' and 'key')
splice(#$col_h, 0, 2);
# print out the header row for the table (I'm doing a simple tab-delim'd table)
print STDERR "Date\t";
# for each NUMBER from NUMBER1 ... NUMBERn
foreach my $c (#$col_h) {
# print "keyID NUMBERn"
map { print STDERR "$_ $c\t" } #key_list;
}
print STDERR "\n";
# Now print out the data itself. Sort by date...
foreach my $date (sort keys %$data) {
print STDERR "$date\t";
# for each NUMBER1 ... NUMBERn
foreach my $header (#$col_h) {
foreach my $key (#key_list) {
## print out the value OR - if there is no value
print STDERR ( $data->{$date}{$key}{$header} || "-" ) . "\t";
}
}
print STDERR "\n"; # end of the table row
}
Output (with tabs expanded for display purposes):
Date 1a number1 2b number1 3c number1 4d number1 1a number2 2b number2 3c number2 4d number2
22.12.2013 1a1-34567 2b1-3249871 3c1-1234567 4c1-3249871 1a2-1234567 2b2-4597134 - 4c2-4597134
22.13.2013 1a1-34567 - 3c1-1234567 4c1-3249871 1a2-1234567 2b2-4597134 3c2-1234567 4c2-4597134
I was able to put together code that works using great answer from "i alarmed alien" .
First thing that is different is that my data are formatted as array of arrays in transposed way.
$arr1 = [ '2013-12-22', '2013-12-22' ];
$arr2 = [ 'Number1','Number2'];
$arr3 = [ '2328942', '679204'];
$arr4 = [ '1450398', '436713'];
Also transformed data should be saved in an array. I've written this piece of code. ( It's far from perfect, if there are any suggestions how to improve it further I'd be happy to hear those.)
####################
#transpose data
my $datas = $args{DATA};
my $headers = $args{HEADERS};
my #rows = ();
my #transposed = ();
for my $row (#$datas) {
for my $column (0 .. $#{$row}) {
push(#{$transposed[$column]}, $row->[$column]);
}
}
#################################
my #arr = #transposed;
# first, define headers.
my $col_h = $args{HEADERS};
my $data;
my $key_list;
foreach (#arr) {
my %hash;
# use a hash slice with the column header array as keys
# and the array as the values
#hash{#$col_h} = #$_;
# store this hash in a data hash indexed by date then key
$data->{ $hash{date} }{ $hash{key} } = \%hash;
# compile a separate hash with the keys in it
$key_list->{ $hash{key} }++;
}
# make a sorted list of keys, ready for printing
my #key_list = sort keys %$key_list;
# remove the first two elements from the column headers ('date' and 'key')
splice(#$col_h, 0, 2);
my #output;
my #header;
# print out the header row for the table (I'm doing a simple tab-delim'd table)
#print STDERR "Date\t";
push(#header, "Date\t");
# for each NUMBER from NUMBER1 ... NUMBERn
foreach my $c (#$col_h) {
# print "keyID NUMBERn"
map { push (#header,"$_ $c\t" )} #key_list;
#map { print STDERR "$_ $c\t" } #key_list;
}
#print STDERR "<br>";
push (#output,\#header );
my $row;
my $column;
# Now print out the data itself. Sort by date...
foreach my $date (sort keys %$data) {
#print STDERR "$date\t";
$row++;
my #line;
push(#line, "$date");
# for each NUMBER1 ... NUMBERn
foreach my $header (#$col_h) {
foreach my $key (#key_list) {
## print out the value OR - if there is no value
$column++;
push (#line,( $data->{$date}{$key}{$header} || "-" ) . "\t");
#print STDERR ( $data->{$date}{$key}{$header} || "-" ) . "\t";
}
}
print STDERR "<br>"; # end of the table row
$column = 0;
push (#output,\#line );
}
my $x = 1;
return #output;
}
This code works but it's little ugly. Please let me know If there is cleaner/better way to do this.

What pitfalls does this Perl code have?

I have written some code to print formatted arrays (first line = no of inputs, second line = max width of numbers). The star can be any sort of marker to differentiate some elements from the rest.
$ cat inp.txt
6
2
1 *
2
3
4
9
12 *
$ cat inp.txt | ./formatmyarray.pl
____ ____ ____ ____ ____ ____
| * | | | | | * |
| 1 | 2 | 3 | 4 | 9 | 12 |
|____|____|____|____|____|____|
$
fomatmyarray.pl
#!/usr/bin/perl
use warnings;
use strict;
my $spc = q{ };
my $und = q{_};
my $sep = q{|};
my $end = "\n";
my #inp = <STDIN>;
my $len = $inp[0];
my $wid = $inp[1];
chomp $wid;
sub printwall {
my($left, $middle, $l, $w) = #_;
for(0..($l - 1)) {
if ($_ == 0) { print $left; }
for(1..($w + 2)) { print $middle; }
print $left;
}
print $end;
return;
}
sub printchar {
my($left, $middle, $l, $w) = #_;
for(0..($l - 1)) {
if ($_ == 0) { print $left; }
my #temp = split ' ', $inp[$_ + 2];
my $star = 0;
if (($#temp) >= 1) { $star = 1; }
my $mid = sprintf "%d", (($w + 2) /2);
for(1..($w + 2)) {
if (($_ == $mid) && ($star == 1)) { print "*"; }
else { print $middle; }
}
print $left;
}
print $end;
return;
}
sub printnum {
my($left, $middle, $l, $w) = #_;
for(0..($l - 1)) {
if ($_ == 0) { print $left; }
my #temp = split ' ', $inp[$_ + 2];
my $format = join '', q{%}, $w, q{d};
my $num = sprintf($format, $temp[0]);
print join '', $middle, $num, $middle, $left;
}
print $end;
return;
}
printwall($spc, $und, $len, $wid);
printchar($sep, $spc, $len, $wid);
printnum ($sep, $spc, $len, $wid);
printwall($sep, $und, $len, $wid);
I already checked it with Perl::Critic but that will only tell me the syntactical problems (which I have already corrected). Are there any problems that you see with this code. Something an experienced Perl programmer would do differently?
Any comments or suggestions are welcome.
Several suggestions in here. Hope this is helpful.
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
my $SPC = q{ };
my $UND = q{_};
my $SEP = q{|};
my $END = "\n";
main();
sub main {
# Try to keep run options and core input data separate from each other.
GetOptions('max=i' => \my $max_n);
# Parse input at the outset so that subsequent methods
# don't have to worry about such low-level details.
my $inp = parse_input();
# Prune the input array at the outset.
# This helps to keep subsequent methods simpler.
splice #$inp, $max_n if $max_n;
# Don't require the user to compute max width.
my $wid = determine_width($inp);
# The format string can be defined at the outset.
my $fmt = join '', $SEP, $SPC, '%', $wid, 's', $SPC;
# You can print both data and stars using one method.
print_border($inp, $wid, $SPC);
print_content($inp, $fmt, $_) for qw(star data);
print_border($inp, $wid, $SEP);
}
sub parse_input {
my #parsed;
# Using <> provides more flexibility than <STDIN>.
while (<>){
chomp;
my ($value, $star) = split;
$star = $SPC unless defined $star;
push #parsed, { data => $value, star => $star }
}
return \#parsed;
}
sub determine_width {
my $inp = shift;
my $wid = 0;
for (#$inp){
my $len = length $_->{data};
$wid = $len if $len > $wid;
}
return $wid;
}
# Because we did work at the outset to create a data structure
# that represents our goals conveniently, generating output
# is much simpler.
sub print_border {
my ($inp, $wid, $wall_sep) = #_;
print $wall_sep, $UND x ($wid + 2) for #$inp;
print $wall_sep, $END;
}
sub print_content {
my ($inp, $fmt, $mode) = #_;
printf $fmt, $_->{$mode} for #$inp;
print $SEP, $END;
}
There's a lot of room for improvement here (I'll update this answer as and when I have time).
Let's start off with the inputs. You should not have to specify the number of entries or their maximum length as Perl can infer that for you:
my $entries = my #entries = <STDIN>;
Don't forget about CPAN.
For instance, consider Text::ASCIITable.
The return statements would not appear in most people's code - a sub returns when it reaches the end (but see discussion in comments).
In printwall, I'd unconditionally print the first left wall outside the loop; ditto the other functions.
I'm not convinced I'd read all the data into #inp as shown. More likely, I'd use:
my $num = <STDIN>; # Or, more likely, just <>
my $wid = <STDIN>;
my #inp = <STDIN>;
This would clean up the $inp[$_ + 2] in the functions.
I'd probably pass the array to the functions, rather than using global variables - globals are grubby in Perl as everywhere else.
The count of the number of values is not needed in the input. With the array containing just the data to be printed, you can iterate over each member of the array in the functions with a suitable foreach, improving its Perlishness.
In printnum, you can build the format string once (not each iteration).
This:
my $mid = sprintf "%d", (($w + 2) /2);
is a funny way of writing:
my $mid = int(($w + 2) / 2);
I'd probably use a regex to find the star; it isn't clear whether you should print a '*' if any character is found, or if you should print the character that is found.
I'd probably be using a single format to deal with the stars:
my $fmt = sprintf "%*s%%c%*s%c", $wid, $middle, $wid, $middle, $left;
I might need to tune one of the $wid values to allow for even widths, but the output would be:
" %c |"
You can then simply print each cell with a blank or a '*' for the value using the format.
Similarly, in printnum, I'd be generating a simple format string like " %2d |" to print each number - and I'd generate that format once.
Etc.

Resources