This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Formatting output with 'printf' in Perl
my #selections = ("Hamburger","Frankfurter","French Fries","Large Coke","Medium Coke","Small Coke","Onion Rings");
my #prices = (3.49, 2.19, 1.69, 1.79, 1.59, 1.39, 1.19);
my #quantity = (3, 0, 0, 4, 0, 0, 8);
printf("%s %10s %12s %10s\n", "Qty", "Desc.", "Unit \$", "Total");
for($meh = 0; $meh <= 6; $meh++)
{
if($quantity[$meh] != 0)
{
printf("%d %10s %9.2f %7.2f\n", $quantity[$meh], $selections[$meh], $prices[$meh], $prices[$meh]*$quantity[$meh])
}
}
I can't figure out how to make the columns line up. I followed the suggestions of another post, but it still isn't working.
The problem is that your strings are more than 10 characters long, and Perl won't cut them unless you specify a maximum width, which is given after the dot for strings (%10.10s). Also, you may want to use a negative number so they become aligned to the left (%-10.10s).
If you want the columns to be exactly aligned based on dynamic input data, you need to make two passes over the rows. The first time through, record the maximum length of each column. Then construct a format string using those lengths. Finally, print each row using that format string.
use strict;
use warnings;
my #selections = ("Hamburger","Frankfurter","French Fries","Large Coke","Medium Coke","Small Coke","Onion Rings");
my #prices = (3.49, 2.19, 1.69, 1.79, 1.59, 1.39, 1.19);
my #quantity = (3, 0, 0, 4, 0, 0, 8);
my #rows;
push #rows, ["Qty", "Desc.", "Unit \$", "Total"];
# construct table data as a two-dimensional array
for (my $meh = 0; $meh < #selections; $meh++) {
next unless $quantity[$meh];
push #rows, [$quantity[$meh], $selections[$meh], $prices[$meh], $prices[$meh]*$quantity[$meh]];
}
# first pass over rows: compute the maximum width for each column
my #widths;
for my $row (#rows) {
for (my $col = 0; $col < #$row; $col++) {
$widths[$col] = length $row->[$col] if length $row->[$col] > ($widths[$col] // 0);
}
}
# compute the format. for this data, it works out to "%-3s %-11s %-6s %-5s\n"
my $format = join(' ', map { "%-${_}s" } #widths) . "\n";
# second pass: print each row using the format
for my $row (#rows) {
printf $format, #$row;
}
That yields this output:
Qty Desc. Unit $ Total
3 Hamburger 3.49 10.47
4 Large Coke 1.79 7.16
8 Onion Rings 1.19 9.52
Long time ago, Perl was mainly used for formatting files. It still has this capabilities although I haven't seen it used in a program since Perl 4.x came out.
Check out the perlform documentation, the format function, and the write function.
I'd give you an example on what the code would look like except I haven't done it in years. Otherwise, use the printf statement. You can limit the size of a text field with a %-10.10s type of format. This says to left justify the string, and pad it out to 10 characters, but not more than 10 characters.
I also suggest you get a book on modern Perl. One that will teach you about references.
I've rewritten your program to use references. Notice that all of the data is now in a single array instead of spread over four separate arrays that you hope you keep the index together.
I can talk about the ENTREE of $item[1] by saying $item[1]->{ENTREE}. It's easier to read and easier to maintain.
Also note that I've changed your for loop. In yours, you had to know that you had seven items. If you added a new item, you'd have to change your loop. In mine, I use $#menu to get the last index of my menu. I then use (0..$#menu) to automatically loop from 0 to the last item in the #menu array.
use strict;
use warnings;
use Data::Dumper;
my #menu = (
{ ENTREE => "Hamburger", PRICE => 3.49, QUANTITY => 3 },
{ ENTREE => "Frankfurter", PRICE => 2.19, QUANTITY => 0 },
{ ENTREE => "French Fries", PRICE => 1.69, QUANTITY => 0 },
{ ENTREE => "Large Coke", PRICE => 1.79, QUANTITY => 4 },
{ ENTREE => "Medium Coke", PRICE => 1.59, QUANTITY => 0 },
{ ENTREE => "Small Coke", PRICE => 1.39, QUANTITY => 0 },
{ ENTREE => "Onion Rings", PRICE => 1.19, QUANTITY => 8 },
);
printf "%-3.3s %-10.10s %-6.6s %s\n\n", 'Qty', 'Desc.', 'Unit $', 'Total';
# Use $#menu to get the number of items in the array instead of knowing it's 6
foreach my $item (0..$#menu) {
# Dereference $menu[$item] to make $menu_item a hash
# This makes the syntax easier to read.
my %menu_item = %{ $menu[$item] };
if ( $menu_item{QUANTITY} ) {
printf "%3d %-10.10s %9.2f %7.2f\n",
$menu_item{QUANTITY}, $menu_item{ENTREE}, $menu_item{PRICE},
$menu_item{QUANTITY} * $menu_item{PRICE};
}
}
OUTPUT:
Qty Desc. Unit $ Total
3 Hamburger 3.49 10.47
4 Large Coke 1.79 7.16
8 Onion Ring 1.19 9.52
Related
I have a hashref that has data tied to days of the calendar year, for example:
my $calendarEntries = { '1' => 'Entry 1', '5' => 'Entry 2', '15' => 'Entry 3' };
I can obtain the day of the year using DateTime:
state $moduleDateTime = require DateTime;
my $dt = DateTime->now('time_zone' => 'America/Chicago');
my $dayOfTheYear = $dt->strftime('%j');
However, I'm trying to figure out the most efficient way to handle situations where the current day does not match any of the days in the hash. I'd like to always "round down" in those situations. E.g. today (which is the 7th day of the year), I'd like to load the entry with the key '5', since it is the most "recent" entry.
Is there a way to select a key in a hashref that is the closest candidate for being <= $dayOfTheYear? If I were using DBD, I could do a query like this:
'SELECT entry WHERE `key` <= ' . $dayOfTheYear . ' ORDER BY `key` DESC LIMIT 1'
But, I'd rather avoid needing to create a database and call it, if I can do something natively in Perl.
One way, expecting many searches
use List::MoreUtils qw(last_value);
my #entries = sort { $a <=> $b } keys %$calendarEntries;
my $nearest_le = last_value { $day >= $_ } #entries;
This returns the last element that is less or equal, for any input, so the key of interest.
The drawback of using simply a hash is that one needs an extra data structure to build. Any library that offers this sort of lookup must do that as well, of course, but those then come with other goodies and may be considerably better performing (depending on how often this is done).
If this 'rounding' need be done a lot for a given hash then it makes sense to build a lookup table for days, associating each with its nearest key in the hash.† ‡
If #entries is sorted descending ($b <=> $a) then the core List::Util::first does it.
† For example
my %nearest_le;
my #keys = sort { $a <=> $b } keys %$calendarEntries;
for my $day (1..366) {
for my $k (#keys) {
if ($k <= $day) {
$nearest_le{$day} = $k;
}
else { last }
}
};
This enumerates days of the year, as specified in the question.
‡ If this were needed for things other than the days (366 at most), where long lists may be expected, a better algorithmic behavior is afforded by binary searches on sorted lists (O(log n)).
The library used above, List::MoreUtils, also has lower_bound with O(log n)
Returns the index of the first element in LIST which does not compare less than val.
So this needs a few adjustments, for
use List::MoreUtils qw(lower_bound);
my #keys = sort { $a <=> $b } keys %$calendarEntries;
my $nearest_le = exists $calendarEntries->{$day}
? $day
: $keys[ -1 + lower_bound { $_ <=> $day } #keys ];
A nice simple solution.
use List::Util qw( max );
max grep { $_ <= $dayOfTheYear } keys %$calendarEntries
Notes:
Best to make sure $calendarEntries->{ $dayOfTheYear } doesn't exist first.
You'll need to handle the case where there is no matching key.
It's faster than sorting unless you perform many searches. But even then, we're only dealing with at most 365 keys, so simplicity is key here.
The simplest solution is to simply look up the value for your date, and if it is not found, go down until you find a value. In this sample, I included a rudimentary error handling.
use strict;
use warnings;
use feature 'say';
my $calendarEntries = { '1' => 'Entry 1', '5' => 'Entry 2', '15' => 'Entry 3' };
my $find = shift // 7; # for testing purposes
my $date = get_nearest_below($calendarEntries, $find);
if (defined $date) {
say "Nearest date below to '$find' is '$date'";
} else { # error handling
warn "Nearest date below not found for '$find'";
}
sub get_nearest_below {
my ($href, $n) = #_;
while ($n > 0) { # valid dates are > 0
return $n if defined $href->{$n}; # find a defined value
$n--; # or go to the next key below
}
return undef; # or return error if nothing is found before 0
}
Output:
$ foo.pl
Nearest date below to '7' is '5'
$ foo.pl 12
Nearest date below to '12' is '5'
$ foo.pl 123
Nearest date below to '123' is '15'
$ foo.pl 0
Nearest date below not found for '0' at foo.pl line 13.
I want to find an efficient way (in Perl preferably) to learn the fixed order of a family of words by comparing their order in multiple subsets of the group.
(They are job parameters. There are about 30 different ones. Different jobs need different combinations of parameters & there are only ever a few parameters in each job)
For example, given:
first
second
third
sixth
seventh
tenth
first
third
fourth
fifth
sixth
third
fifth
seventh
eighth
ninth
tenth
It should be able to remember the relative order relationships it sees to work out that the order is:
first
second
third
fourth
fifth
sixth
seventh
eighth
ninth
tenth
I have generated lists like:
first.second.third.sixth.seventh.tenth
first.third.fourth.fifth.sixth
third.fifth.seventh.eighth.ninth.tenth
then sorted uniquely + alphabetically and visually compared them, but I have hundreds of different combinations of the 30ish parameters, so it will be a big job to sort through them all and put them together manually.
I think #daniel-tran has answered the "how" in https://stackoverflow.com/a/48041943/224625 and using that and some hackery like:
$order->{$prev}->{$this} = 1;
$order->{$this}->{$prev} = 0;
I've managed to populate a hash of hashes with a 1 or a 0 for each pair of consecutive parameters to say which comes first, like:
$VAR1 = {
'first' => {
'second' => 1,
'third' => 1,
},
'second' => {
'first' => 0,
'third' => 1,
},
'third' => {
'first' => 0,
'second' => 0,
'fourth' => 1,
'fifth' => 1,
'sixth' => 1,
},
'fourth' => {
'third' => 0,
'fifth' => 1,
},
...
but I hit the wall trying to work out what to do in my sort function when it's asked to sort a pair that have never been seen as immediate neighbours, thus don't have a relationship defined.
Is there an easy solution?
Am I going about this the right way?
Is there a better WTDI in the first place?
Thanks,
John
The question you linked to includes another answer using a graph and topological sort. The Graph module is pretty easy to use:
use warnings;
use strict;
use Graph;
my $graph = Graph->new(directed => 1);
my $prev;
while (<DATA>) {
chomp;
$graph->add_edge($prev, $_) if length && length $prev;
$prev = $_;
}
print $_,"\n" for $graph->topological_sort;
__DATA__
first
second
third
sixth
seventh
tenth
first
third
fourth
fifth
sixth
third
fifth
seventh
eighth
ninth
tenth
Output:
first
second
third
fourth
fifth
sixth
seventh
eighth
ninth
tenth
I tried to implement a naive solution myself. I built the %order hash where the values of each key were the elements that followed it. I then created a transitive closure of this structure (i.e. if first was before second and second was before third, then first must be before third). If there was enough information, each key would have a different number of values, and sorting the elements by the number of the values would give the ordered list.
#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };
my #partial = (
[qw[ first second third sixth seventh tenth ]],
[qw[ first third fourth fifth sixth ]],
[qw[ third fifth seventh eighth ninth tenth ]]);
my %order;
my %all;
for my $list (#partial) {
undef #all{ #$list };
undef $order{ $list->[ $_ - 1 ] }{ $list->[$_] }
for 1 .. $#$list;
}
my $changed = 1;
while ($changed) {
undef $changed;
for my $from (keys %order) {
if (my #to = keys %{ $order{$from} }) {
if (my #to2 = map keys %{ $order{$_} }, #to) {
my $before = keys %{ $order{$from} };
undef #{ $order{$from} }{#to2};
$changed = 1 if $before != keys %{ $order{$from} };
}
}
}
}
my %key_counts;
$key_counts{ keys %{ $order{$_} } }++ for keys %order;
warn "Not enough information\n"
if keys %key_counts != keys %order;
say join ' ',
sort { keys %{ $order{$b} } <=> keys %{ $order{$a} } }
keys %order;
output
first second third fourth fifth sixth seventh eighth ninth tenth
This is a direct and simple-minded manual solution.
It collects all elements in given sub-sequences and sorts them. The sorting criterion is the position (index) of compared elements in the first sub-sequence that has both. If none of the sub-sequences have both elements an undecided (zero) is returned from the sort's block.
use warnings;
use strict;
use feature 'say';
use List::MoreUtils qw(uniq firstval);
my #all = qw(ant bug frog cat dog elk); # to draw input (sublists) from
my #s1 = #all[0,1,3,5];
my #s2 = #all[1,2,4,5];
my #s3 = #all[2,3,4];
my #inv = ( # for index comparison
{ map { $s1[$_] => $_ } 0..$#s1 },
{ map { $s2[$_] => $_ } 0..$#s2 },
{ map { $s3[$_] => $_ } 0..$#s3 }
);
my #sorted = sort {
my $fv = firstval { exists $_->{$a} and exists $_->{$b} } #inv;
($fv) ? $fv->{$a} <=> $fv->{$b} : 0;
} uniq #s1, #s2, #s3;
say "#sorted";
The complexity of this isn't as good as it can be since some of the comparisons can (in principle) be avoided but that doesn't show for smaller problems. It prints the correct sequence, and for the posted problem as well (replace #sN arrays with the ones provided in the question).
This code assumes consistent and complete enough subsequences.
For an arbitrary number of subsets of the full list (3 above) the ancillary #inv is built as
my #subseqs = (\#s1, \#s2, \#s3);
my #inv;
for my $rr (#subseqs) {
push #inv, { map { $rr->[$_] => $_ } 0..$#$rr }
}
Say I have a string like:
my $refseq="CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
I want to detect the positions where "-" occurs and the number of contiguous "-". I want to end up with a hash with "-" position as key, and extension length as value, for this example above:
%POSLENGTH = (5 => 1, 8 => 3, 14 => 2, 19 => 1, 27 => 20);
Note that the positions should be given based on the string without "-".
Check for #- array in perlval
my $refseq = "CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
my %POSLENGTH;
$POSLENGTH{ $-[0] +1 } = length($1) while $refseq =~ s/(-+)//;
use Data::Dumper; print Dumper \%POSLENGTH;
output
$VAR1 = {
'14' => 2,
'8' => 3,
'27' => 20,
'19' => 1,
'5' => 1
};
You can do this using the built-in #- and #+ arrays. Together they hold the start and end offsets of the last successful pattern match in element 0 (and of any captures in elements 1 onwards) so clearly the length of the last match is $+[0] - $-[0].
They're documented under Variables related to regular expressions in perldoc perlvar.
I've used Data::Dump here just to display the contents of the hash that is built
On a side note, I'm very doubtful that a hash is a useful structure for this information as I can't imagine a situation where you know the start position of a substring and need to know its length. I would have thought it was better represented as just an array of pairs
use strict;
use warnings;
use Data::Dump;
my $refseq="CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
my %pos_length;
while ( $refseq =~ /-+/g ) {
my ($pos, $len) = ( $-[0] + 1, $+[0] - $-[0] );
$pos_length{$pos} = $len;
}
dd \%pos_length;
output
{ 5 => 1, 9 => 3, 18 => 2, 25 => 1, 34 => 20 }
Can I use 'map' or some similar function to make the codes simpler?
# $animal and #loads are pre-defined somewhere else.
my #bucket;
foreach my $item (#loads) {
push #bucket, $item->{'carrot'} if $animal eq 'rabbit' && $item->{'carrot'};
push #bucket, $item->{'meat'} if $animal eq 'lion' && $item->{'meat'};
}
Are you looking for something like this?
%foods = ( 'lion' => 'meat', 'rabbit' => 'carrot');
# ...
foreach my $item (#loads) {
push #bucket, $item->{$food{$animal}} if $item->{$food{$animal}};
}
This question would be easier to answer authoritatively with a bit more sample data. As it is I need to make a lot of assumptions.
Assuming:
#loads = (
{ carrot => 47, meat => 32, zebras => 25 },
{ carrot => 7, zebras => 81 },
);
and #buckets should look like:
#buckets = ( 47, 32, 7 );
when #animals looks like:
#animals = qw/ rabbit lion /;
Here's a maptastic approach. To understand it you will need to think in terms of lists of values as the operands rather than scalar operands:
my #animals = qw/ rabbit lion /;
my %eaten_by = (
lion => 'meat',
rabbit => 'carrot',
mouse => 'cheese',
);
# Use a "hash slice" to get a list of foods consumed by desired animals.
# hash slices let you access a list of hash values from a hash all at once.
my #foods_eaten = #eaten_by{ #animals };
# Hint: read map/grep chains back to front.
# here, start with map #loads and then work back to the assignment
my #bucket =
grep $_, # Include only non-zero food amounts
map #{$_}{#foods_eaten}, # Hash slice from the load, extract amounts of eaten foods.
map #loads; # Process a list of loads defined in the loads array
Rewritten in a verbose nested loop you get:
my #buckets;
for my $item ( #loads ) {
for my $animal ( #animals ) {
my $amount = $item{ $eaten_by{$animal} };
next unless $amount;
push #buckets, $amount;
}
}
Which one to use? It all depends on your audience--who will be maintaining the code? Are you working with a team of Perl hackers featuring 4 of the perl5porters? use the first one. Is your team composed of one or two interns that come and go with the seasons who will spend 1% of their time working on code of any kind? Use the second example. Likely, your situation is somewhere in the middle. Use your discretion.
Happy hacking!
I have a few arrays of the same length. I want to sort the first array, and make all the others array "sort" accordingly. For example, if the first array is (7,2,9) the second is ("seven","two","nine") and the third is ("VII","II","IX") after the sort (ascendingly according to the first array values) we will have (2,7,9) ("two","seven","nine") and ("II","VII","IX").
How can I do that?
While I agree with eugene y and MvanGeest that usually the best answer is to switch to another data structure, sometimes you might want parallel arrays (or at least, might not be able to avoid them), and there actually is a way to sort parallel arrays in parallel. It goes like this:
my #nums = (7, 2, 9);
my #names = qw(seven two nine);
my #roman = qw(VII II IX);
my #sorted_indices = sort { $nums[$a] <=> $nums[$b] } 0..$#nums;
#$_ = #{$_}[#sorted_indices] for \(#nums, #names, #roman);
That is, generate a list of the indices that correspond to all of the arrays, and then sort them according to the order that will put the "primary" array in order. Once we have the sorted list of indices, re-order all of the arrays to match.
The final line could be written out longhand as
#nums = #nums[#sorted_indices];
#names = #names[#sorted_indices];
#roman = #roman[#sorted_indices];
but I tried to reduce the amount of copy-paste necessary, even at the cost of some slightly hairy syntax. The More You Know...
I know you've already accepted an answer, and there are other really good
answers here, but I would propose something different: don't duplicate your
data. You only need to keep track of the arabic -> roman mapping once -- why
store what are essentially duplicate arrays of numbers, and sort every one?
Just sort the master list and look up the other values in a reference array as needed:
my #roman = qw(0 I II III IV V VI VII VIII IX X);
my #text = qw(zero one two three four five six seven eight nine ten);
my #values = (7, 2, 9);
my #sorted_values = sort #values;
my #sorted_roman = map { $roman[$_] } #sorted_values;
my #sorted_text = map { $text[$_] } #sorted_values;
use Data::Dumper;
print Dumper(\#sorted_values, \#sorted_roman, \#sorted_text);
prints:
$VAR1 = [
2,
7,
9
];
$VAR2 = [
'II',
'VII',
'IX'
];
$VAR3 = [
'two',
'seven',
'nine'
];
In a real environment, I would suggest using libraries to perform the
Roman and textual conversions for you:
use Roman;
my #sorted_roman = map { roman($_) } #sorted_values;
use Lingua::EN::Numbers 'num2en';
my #sorted_text = map { num2en($_) } #sorted_values;
Re-organize the data to a single array for sorting:
my #a = ([7, "seven", "VII"], [2, "two", "II"], ..);
#a = sort { $a->[0] <=> $b->[0] } #a;
Then recreate the original arrays:
my(#a1, #a2, #a3);
for (#a) {
push #a1, shift #$_;
push #a2, shift #$_;
push #a3, shift #$_;
}
As you are discovering, maintaining parallel arrays can be a hassle and error prone. An alternative approach is to keep related information together.
use strict;
use warnings;
# One array-of-hashes instead of three parallel arrays.
my #numbers = (
{ arabic => 7, text => 'seven', roman => 'VII' },
{ arabic => 2, text => 'two', roman => 'II' },
{ arabic => 9, text => 'nine', roman => 'IX' },
);
#numbers = sort { $a->{arabic} <=> $b->{arabic} } #numbers;