Near the Christmas in 2022, I played a data engineering challenge called "Hanukkah of Data 5783". You can find it on https://hanukkah.bluebird.sh/5783 . Players can download data of either .CSV, .JSON or .SQLITE format.
The first* task is like this:
... And their phone number was their last name spelled out. I didn’t know what that meant, but apparently before there were smartphones, people had to remember phone numbers or write them down. If you wanted a phone number that was easy-to-remember, you could get a number that spelled something using the letters printed on the phone buttons: like 2 has “ABC”, and 3 “DEF”, etc."
* There is a "zeroth" task on the calendar year.
I didn't mind perish my skill on SQL, but these kinds of things immediately I thought of Perl, maybe there would be a subroutine like this:
sub phonebutton { return 2 if $_[0] =~ /[abc]/; return 3 if $_[0] =~ /[def]/; return 4 if $_[0] =~ /[ghi]/; return 5 if $_[0] =~ /[jkl]/; return 6 if $_[0] =~ /[mno]/; return 7 if $_[0] =~ /[pqrs]/; return 8 if $_[0] =~ /[tuv]/; return 9 if $_[0] =~ /[wxyz]/; }
But what tools in Perl ecosystem could help me?
I found the CPAN module Data::Table. It is a tool for .xls and .csv file.
I use Data::Table to solve all those challenges. Data::Table has a very nice cookbook by its main author and can be found on https://sites.google.com/site/easydatabase/ .
Spoiler Alert: The following is how I solved one of the "Hanukkah of Data 5783" challenge. Here I suggest you reading the Data::Table cookbook (it is really nicely formatted and organized!) and try the Hanukkah of Data(it is really fun!) on yourself.
Puzzle statement on Day 5: https://hanukkah.bluebird.sh/5783/5/
The point is to find a lady living in Queens Village and having a habit of buying a lot of pet food. There is a category of product started with "PET". So my strategy was looking for the top few people spending the greatest amount of money or buying largest amount of pet food over the course of time.
I had to combine three tables: the table of customer details (including name, phone number, birthday, living area, customer ID), the table of orders and the table of items on each item. To combine tables, there is usually a JOIN operation. Relational database users know there are LEFT_JOIN, RIGHT_JOIN, INNER_JOIN and FULL_JOIN(OUTER_JOIN). Data::Table supports these JOINs.
Use of subTable() makes the operations less consuming.
sort([header], Data::Table::NUMBER, Data::Table::DESC) helped me to look for those pet lovers.
Here is my code:
# Day 5 of "Hanukkah of Data 5783" use v5.30.0; use warnings;use Data::Table;
use List::Util qw/sum/;my $t = Data::Table::fromCSV("noahs-customers.csv");
my $u = Data::Table::fromCSV("noahs-orders.csv");
my $v = Data::Table::fromCSV("noahs-orders_items.csv");my $t_p = $t->match_pattern_hash(
'$_{citystatezip} =~ /Queens Village/'
);say $t_p->nofRow;
my $v_p = $v->match_pattern_hash(
'$_{sku} =~ /^PET/'
);my $u_p = $u->subTable([0..$u->nofRow-1], ["orderid", "customerid"]);
my $r = $t_p->join($u_p, Data::Table::INNER_JOIN, ['customerid'], ['customerid'])
->join($v_p, Data::Table::LEFT_JOIN, ['orderid'], ['orderid'])
->group(['customerid'], ['qty'], [ sum ], ['numofpetfood']);$r->sort('numofpetfood', Data::Table::NUMBER, Data::Table::DESC);
say $r->elm(0, "name"), " ", $r->elm(0, "phone"), " ", $r->elm(0, "numofpetfood");
say $r->elm(1, "name"), " ", $r->elm(1, "phone"), " ", $r->elm(1, "numofpetfood");
The difference of amount of purchases between the first pet food frequent buyer and the second pet food frequent buyer is huge. So there is no doubt who the person the puzzle is looking for.
I have put my solution of each day as gists on GitHub.
The official document provided by Web::Scraper is quite clear. I copied the style and comments and made up a script :
use v5.24.0; use URI; use Web::Scraper; # First, create your scraper block my $modules = scraper { # Parse all TDs inside 'table' with classname equal "name", store them into # an array 'authors'. We embed other scrapers for each TD. process 'table td[class="name"]', "modules[]" => scraper { # And, in each TD, # get the URI of "a" element process "a", uri => '@href'; # get text inside "small" element process "a", name => 'TEXT'; }; };my $res = $modules->scrape( URI->new("https://metacpan.org/author/CYFUNG") );
# iterate the array 'modules'
for my $module (@{$res->{modules}}) {
# output:
# Map-Tube-Hongkong-0.03 https://metacpan.org/dist/Map-Tube-Hongkong
# Math-Abacus-0.05 https://metacpan.org/dist/Math-Abacus
# Math-Cryptarithm-v0.20.2 https://metacpan.org/dist/Math-Cryptarithm
# Math-Permutation https://metacpan.org/dist/Math-Permutation
say "$module->{name}\t$module->{uri}";
}
It may not be obvious for some newcomers. In this case, a data dumping suite like Data::Dumper/Data::Dump/Data::Printer will be your friend.
Web::Scraper can do web scraping task for gracefully. But it is common that a web page is not organized by the structure we want. Here is a script I used at work.
# Perl script for scraping list of shopping centres
# in New Territories, Hong Kong from wikipediause URI;
use Web::Scraper;
use Encode;
use v5.24.0;
use warnings;
binmode STDOUT, ':utf8';my $list_of_shopping_centres_in_NT
= 'https://zh.m.wikipedia.org/wiki/新界商場列表';# The web page is like:
# big region A (h3)
# region A.1 (h4)
# table of shopping centres in A.1
# region A.2 (h4)
# table of shopping centres in A.2
# ...
# big region B (h3)
# region B.1 (h4)
# table of shopping centres in B.1
# region B.2 (h4)
# table of shopping centres in B.2
# ...my $wikipedia = scraper {
process 'body', "body[]" => scraper {
process 'h4', "region[]" => 'TEXT',
process '//h4/following::table', "tbl[]" => scraper {
process 'tr', "tr[]" => scraper {
process 'td', "content[]" => 'TEXT',
};
}
}
};my $res = $wikipedia->scrape( URI->new($list_of_shopping_centres_in_NT) );
my $counter = 0;my @regions;
foreach my $v ($res->{'body'}->@*) {
@regions = $v->{'region'}->@*;
# remove the word "edit" in Chinese
s/\x{007f16}\x{008f91}//u foreach @regions;
}# The Tricky Part
use utf8;
use List::Util qw(first);
# '馬鞍山' is the region preceding h3 region '大埔', which has no h4 followers
my $idx_a = first { $regions[$_] eq '馬鞍山' } 0..$#regions;
splice @regions, $idx_a+1, 0, '大埔';
# '米埔' is the region preceding h3 region '屯門', which has no h4 followers
my $idx_b = first { $regions[$_] eq '米埔' } 0..$#regions;
splice @regions, $idx_b+1, 0, '屯門';foreach my $v ($res->{'body'}->@*) {
foreach my $u ($v->{'tbl'}->@*) {
foreach my $w ($u->{'tr'}->@*) {
my $ti = $w->{'content'}->[0];
my $region = $regions[$counter];
# output is like:
# 康盛花園商場 將軍澳
# 翠林新城 將軍澳
# 慧安商場 將軍澳
# 慧星匯 將軍澳
# ...
say $ti, "\t", $region if defined($ti) && defined($region);
}
$counter++;
}
}
Since the above example is not friendly to non-Chinese reader, I made up an example scraping a similar Wikipedia page, list of schools in Perth of Western Australia:
# Perl script for scraping list of schools in Perth, Australiause URI;
use Web::Scraper;
use Encode;
use v5.24.0;
use warnings;
binmode STDOUT, ':utf8';my $list_of_schools_in_Perth
= 'https://en.wikipedia.org/wiki/List_of_schools_in_the_Perth_metropolitan_area';my $wikipedia = scraper {
process 'body', "body[]" => scraper {
process 'h3', "type[]" => 'TEXT',
process '//h3/following::table', "tbl[]" => scraper {
process 'tr', "tr[]" => scraper {
process 'td', "content[]" => 'TEXT',
};
}
}
};my $res = $wikipedia->scrape( URI->new($list_of_schools_in_Perth) );
my $counter = 0;my @types;
foreach my $v ($res->{'body'}->@*) {
@types = $v->{'type'}->@*;
s/\[edit\]//u foreach @types;
}foreach my $v ($res->{'body'}->@*) {
foreach my $u ($v->{'tbl'}->@*) {
foreach my $w ($u->{'tr'}->@*) {
my $ti = $w->{'content'}->[0];
my $type = $types[$counter];
say $ti, "\t", $type if defined($ti) && defined($type);
}
$counter++;
}
}
It is worth noting that more dedicated or dynamic web scraping task can be done by the famous package Selenium, which has a Perl port WWW::Selenium.
Long time ago I claimed in front of a friend I would write a short introduction to graph theory, but I had been not able to figure out where I should start. Neither I would try today. The mathematical objects graphs, or the abstract data structures graphs, are full of interesting behaviors being studied in the discrete math subdiscipline graph theory. The CPAN module Graph is designed to empower Perl programmers doodle with undirected graphs and directed graphs (and also multigraphs and hypergraphs - not going to visit these functionalities here).
use v5.30.0;
use warnings;
use Graph::Undirected;my $g = Graph::Undirected->new;
$g->add_vertices(2..20);for my $x (2..19) {
for my $y ($x+1..20) {
$g->add_edge($x, $y) if !($x % $y) || !($y % $x);
}
}my @cc = $g->connected_components;
my $mst = $g->MST_Kruskal;
use Data::Printer;
p @cc;
p $mst;
A graph is basically consist of its vertices and edges. In the Graph module, you can use scalar values or variables as vertices. In the above code snipplet, the graph vertices are "labelled" with integers from 2 to 20, then two vertices are connected by an edge if a number is divisible by another number.
Result:
[
[0] [
[0] 18,
[1] 6,
[2] 3,
...
[13] 14,
[14] 7
],
[1] [
[0] 13
],
[2] [
[0] 19
],
[3] [
[0] 17
],
[4] [
[0] 11
]
]
10=2,10=5,12=2,14=2,14=7,15=3,16=2,18=2,2=20,2=4,2=6,2=8,3=6,3=9 (Graph)
Let see an example of graph vertices labelling with variables. Suppose the data structure [ $name0, [@array_of_names] ] represents the person of $name0 have friends with names in @array_of_names.
use v5.30.0; use warnings; use Graph::Undirected;my $alice = ["Alice", ["Bob"]];
my $bob = ["Bob", ["Alice", "Cathy", "David"]];
my $david = ["David", ["Bob"]];my $g = Graph::Undirected->new;
$g->add_vertices($alice, $bob, $david);for my $x ($g->vertices) {
for my $y ($g->vertices) {
$g->add_edge($x, $y) if grep($_ eq $y->[0], $x->[1]->@*)
&& grep($_ eq $x->[0], $y->[1]->@*);
}
}my @cc = $g->connected_components;
say scalar @cc; #1 These 3 people are in a community.
We may also express our idea in the last line as: say "Alice, Bob and David are in a community" if $g->is_connected;
In an ideal world, your friends treat you as a friend. Let us face a non-ideal world.
use v5.30.0; use Graph::Directed; use Graph::Undirected; use warnings;my $alice = ["Alice", ["Bob"]];
my $bob = ["Bob", ["Alice", "Cathy"]];
# today Bob is no longer friendly towards David
my $cathy = ["Cathy", []];
my $david = ["David", ["Bob"]];my $g = Graph::Directed->new;
$g->add_vertices($alice, $bob, $cathy, $david);for my $x ($g->vertices) {
for my $y ($g->vertices) {
$g->add_edge($x, $y) if grep($_ eq $y->[0], $x->[1]->@*);
}
}my $gu = Graph::Undirected->new;
$gu->add_vertices($g->vertices);
for my $x ($gu->vertices) {
for my $y ($gu->vertices) {
$gu->add_edge($x, $y) if $g->has_edge($x, $y) && $g->has_edge($y, $x);
}
}say "People live in a connected world" if $g->undirected_copy->is_connected;
# printed
say "People are in a community" if $gu->is_connected;
# not printed :(
Finally is my recent use case of the module. It is from The Weekly Challenge 209 Task 2. You can read the problem statement here.
The main strategy in the following code is treating each vertex as an email address, connecting two email addresses if they are belonged to the same account. Then find out each connected component of the graph is an account after merging. (I modified the name of the account, originally it is only "A" and "B"; I think the essence of the scenario is more obvious.)
use v5.30.0; use warnings; use Graph::Undirected;my @accounts1 = ( ['Alex', 'a1@a.com', 'a2@a.com'],
['Bob', 'b1@b.com'],
['Alexender', 'a3@a.com', 'a1@a.com'] );
my @accounts2 = ( ['Alex Smith', 'a1@a.com', 'a2@a.com'],
['Bob', 'b1@b.com'],
['Alex Wicks', 'a3@a.com'],
['Bobby', 'b2@b.com', 'b1@b.com'] );sub merge_acc {
my @accs = @_;
my %email;
for my $acc (@accs) {
for my $i (1..$acc->$#*) {
$email{$acc->[$i]} = $acc->[0];
}
}
my $g = Graph::Undirected->new;
$g->add_vertices(keys %email);
for my $acc (@accs) {
for my $i (1..$acc->$#*-1) {
$g->add_edge($acc->[$i], $acc->[$i+1]);
# can be enhanced to add edges between more emails
}
}
my @cc = $g->connected_components;
my @ans = ();
for my $c (@cc) {
unshift $c->@*, $email{$c->[0]};
push @ans, $c;
}
return @ans;
}
my @result1 = merge_acc(@accounts1);
my @result2 = merge_acc(@accounts2);=pod
use Data::Printer;
p @result1;
p @result2;# The account name chosen and the order
# of the email addresses is not
# the same each time.[
[0] [
[0] "Alexender",
[1] "a1@a.com",
[2] "a2@a.com",
[3] "a3@a.com"
],
[1] [
[0] "Bob",
[1] "b1@b.com"
]
]
[
[0] [
[0] "Bobby",
[1] "b2@b.com",
[2] "b1@b.com"
],
[1] [
[0] "Alex Smith",
[1] "a1@a.com",
[2] "a2@a.com"
],
[2] [
[0] "Alex Wicks",
[1] "a3@a.com"
]
]
People who want to study the introduction of graph theory can read the following free resources:
https://www.whitman.edu/mathematics/cgt_online/book/
And there is a nice drawing tool from the Elm language community:
https://erkal.github.io/kite/
Notice
Thanks to demerphq and Sebastian Schleussner's comments, we should visit a better and easy way to do a regex matching in Perl now. It is not Regexp::Assemble introduced two weeks before - the Perl compiler after version 5.10 has already done the optimization for us! But we have to use the correct syntax. To see what happens, the bottom of this post gives a comparison of regexes for Roman numerals again; we give two more players in the race, /^($r_str_combine)$/ [given my $r_str_combine = join "|", @roman;] and $rx = sprintf qr/^(?:%s)$/, join "|", @roman. We can see the former has almost the same performance as Regexp::Assemble, while the latter is usually the fastest. We do not need to import any modules and get a fast result!
Today let us have a short tour on two modules which can perform a similar function: text searching in MS Office Word documents.
Well, we know there are two common formats of MS Office Word document: .docx and the earlier .doc.
Text::Extract::Word deals with .doc. You can use the function get_all_text to get all text (I'm a verbose tour guide...), or use its object oriented interface which get the text in different location of a document:
# taken from synopsis of the module: my $file = Text::Extract::Word->new("test1.doc"); my $text = $file->get_text(); my $body = $file->get_body(); my $footnotes = $file->get_footnotes(); my $headers = $file->get_headers(); my $annotations = $file->get_annotations(); my $bookmarks = $file->get_bookmarks();
MsOffice::Word::Surgeon deals with .docx. Besides functionalities to extract text, you can also replace text by regular expression, and write a new .docx file.
Here comes a use case of the two modules. I was maintaining a collection of curricula vitae and database of candidate details, but due to a coding bug, some of the CV were missing or overwritten by others' CV. The CVs are in .doc, .docx or .pdf format, and have filenames as CV[number].[extension]. I use Text::Extract::Word and MsOffice::Word::Surgeon to check the MS Office documents.
Note that in the database, since I am physically located in Hong Kong, each candidate name is stored with Chinese characters and English alphabets. And names of some people consist of only two Chinese characters (some, like me, have 3; some people have 4(two characters for the surname, two characters for the given name)), so I chose to take the first two English words in the candidate name field and checked whether the two words are in the corresponding document. ID of each suspicious record will be printed.
Script one:
use utf8; use DBI; use Text::Extract::Word qw(get_all_text); use v5.30.0;for my $num (1..999) {
search_cv($num) if -e "CV".$num.".doc" || -e "CV".$num.".DOC";
}sub search_cv {
my $cv_id = $_[0];
my $filename = "CV".$cv_id.".doc";my $dsn = "DBI:mysql:database=cvdb;host=127.0.0.1";
my $dbh = DBI->connect($dsn, 'mydatabaseadmin', 'mypassword', { mysql_enable_utf8 => 1 });my $fullname;
my $first_two_eng;
my $sth = $dbh->prepare("SELECT name FROM candidate WHERE id=".$cv_id);
$sth->execute;
while (my $ref = $sth->fetchrow_hashref) {
$fullname = $ref->{'name'};
}$fullname !~ s/[^[:ascii:]]//g;
my $engname = $fullname;
my $second_space = index($engname, " ", index($engname, " ")+1 );
my $first_two_eng = ($second_space != -1) ? (substr $engname, 0, $second_space) : (substr $engname, 0);
my $found;
my $text = get_all_text($filename);
$found = index($text, $first_two_eng);if ($found != -1) {
# say "found: ". $cv_id;
}
else {
say "SUSPICIOUS: ". $cv_id;
}
}
Script two
The above is just a very straightforward use case of these two modules. You may explore their POD and use them to suit your need!use utf8;
use DBI;
use MsOffice::Word::Surgeon;
use v5.30.0;for my $num (1..999) {
search_cv($num) if -e "CV".$num.".docx";
}sub search_cv {
my $cv_id = $_[0];
my $filename = "CV".$cv_id.".docx";my $dsn = "DBI:mysql:database=cvdb;host=127.0.0.1";
my $dbh = DBI->connect($dsn, 'mydatabaseadmin', 'mypassword', { mysql_enable_utf8 => 1 });my $fullname;
my $first_two_eng;
my $sth = $dbh->prepare("SELECT name FROM candidate WHERE id=".$cv_id);
$sth->execute;
while (my $ref = $sth->fetchrow_hashref) {
$fullname = $ref->{'name'};
}$fullname !~ s/[^[:ascii:]]//g;
my $engname = $fullname;
my $second_space = index($engname, " ", index($engname, " ")+1 );
my $first_two_eng = ($second_space != -1) ? (substr $engname, 0, $second_space) : (substr $engname, 0);
my $found;
my $surgeon = MsOffice::Word::Surgeon->new(docx => $filename);
my $text = $surgeon->document->plain_text;
$found = index($text, $first_two_eng);
if ($found != -1) {
# say "found: ". $cv_id;
}
else {
say "SUSPICIOUS: ". $cv_id;
}}
# For PDF text search, I will introduce modules later.
Comparison of Regexes for Roman Numerals
use v5.30.0;
use List::Util qw/shuffle sample any/;
use Regexp::Assemble;
use Regexp::Trie;
use feature 'say';my @roman = qw/I II III IV V
VI VII VIII IX X
XI XII XIII XIV XV
XVI XVII XVIII IXX XX/;sub repr {
return sample int 4*rand(),
shuffle('I' x (int 4*rand()), 'V', 'X');
}my $size = 1000;
sub c0 {
my $count = 0;
for (1..$size) {
my $letters = repr();
$count++ if any {$letters =~ /^$_$/} @roman;
}
return $count;
}my $ra = Regexp::Assemble->new;
$ra->anchor_line;
$ra->add(@roman);
my $ra_computed = $ra->re;sub c1 {
my $count = 0;
for (1..$size) {
$count++ if repr() =~ $ra_computed;
}
return $count;
}my $rt = Regexp::Trie->new;
$rt->add($_) for @roman;
my $rt_computed = $rt->regexp;sub c2 {
my $count = 0;
for (1..$size) {
$count++ if repr() =~ /^$rt_computed$/;
}
return $count;
}
my $r_str_combine = join "|", @roman;sub cn {
my $count = 0;
for (1..$size) {
$count++ if repr() =~ /^($r_str_combine)$/;
}
return $count;
}my $rx = sprintf qr/^(?:%s)$/, join "|", @roman;
sub cx {
my $count = 0;
for (1..$size) {
$count++ if repr() =~ $rx;
}
return $count;
}say c0()/$size;
say c1()/$size;
say c2()/$size;
say cn()/$size;
say cx()/$size;
use Benchmark q/cmpthese/;
cmpthese(10_000, {
RAW => sub {c0},
Assemble => sub {c1},
Trie => sub {c2},
naive => sub {cn},
QR => sub {cx}
});
Result:
0.705
0.691
0.68
0.681
0.708
Rate RAW Trie naive Assemble QR
RAW 42.7/s -- -94% -94% -94% -95%
Trie 669/s 1468% -- -6% -7% -23%
naive 711/s 1565% 6% -- -2% -18%
Assemble 724/s 1595% 8% 2% -- -17%
QR 867/s 1932% 30% 22% 20% --
Thanks for pointing out the optimization done by the Perl compiler! I will post a corrected performance comparison next week.
]]>Date of Latest Release: Feb 04, 2023
Distribution: Quiq
Module version: 1.207
Main Contributors: Frank Seitz(FSEITZ)
License: The Artistic License
Source: https://github.com/s31tz/Quiq
Quiq is a "Perl class library for rapid development". The modules are "designed according to uniform principles" (translated from German).
Quiq contains 234 (Ooooops!!!) classes at the time this post is being written and their descriptions are mostly written in German.
It contains several text-based document/markup language code writers, namely Quiq::Tag (for XML), Quiq::Html::MODULES, Quiq::MediaWiki::Markup, Quiq::Css, Quiq::LaTex::MODULES; some network functionalities including Quiq::Http::MODULES, Quiq::Ssh and Quiq::Socket ; some tiny tools like Quiq::Color and Quiq::Stopwatch; some system tools like Quiq::Path, Quiq::TempFile and Quiq::Process; some modules for meta-development including Quiq::Hash and Quiq::Object; some modules for general use like Quiq::Converter, Quiq::Math and Quiq::Epoch. ...
use Quiq::Css; my $rule1 = Quiq::Css->rule('p.abstract', fontStyle => 'italic', marginLeft => '0.5cm', marginRight => '0.5cm', );my @prop = (fontColor => 'red', textWeight => 'bold');
my $prop2 = Quiq::Css->properties(@prop);
my $rule2 = Quiq::Css->rule('p.caution', @prop);
my $rule2_1 = Quiq::Css->oneLine($rule2);say $rule1;
say $prop2;
say "";
say $rule2;
say $rule2_1;
Output:
p.abstract {
font-style: italic;
margin-left: 0.5cm;
margin-right: 0.5cm;
}font-color: red; text-weight: bold;
p.caution {
font-color: red;
text-weight: bold;
}p.caution { font-color: red; text-weight: bold; }
For HTML, one of the modules is Quiq::Html::Tag :
use Quiq::Html::Tag; my $str1 = $h->tag('img',src=>'URL1'); my $str2 = $h->tag('a',href=>'URL2',title=>'WEBSITE',"Link",); my $str3 = $h->tag('a',href=>'URL2', $str1); my $str4 = $h->tag('h3',"My Section");my $str5 =
$h->cat(
['doctype'],
['comment',-nl=>2,'Copyright Anonymous'],
['HTML',
['HEAD',
['TITLE','My Homepage'],
['STYLE',q|
.text { color: red; }
|],
],
['BODY',
['H1','Hello World!'],
['P',class=>'text',q|
I am called Anonymous
and this is my homepage.
|],
],
]
);say $str1;
say $str2;
say $str3;
say $str4;
say $str5;
There are Quiq::LaTeX::Code and Quiq::TeX::Code. Here comes handy shortcut for a LaTeX document:
use Quiq::LaTeX::Document; use Quiq::LaTeX::Code; my $body = 'Hello World' ."\n\n" .'\textlang{german}{Morgen früh: Groß oder klein?}'; my $l = Quiq::LaTeX::Code->new; my $doc = Quiq::LaTeX::Document->new( language => 'english', body => $body ); say $doc->latex($l);
Output:
\documentclass[english,a4paper]{scrartcl}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage[utf8]{inputenc}
\usepackage{babel}
\usepackage{geometry}
\usepackage{microtype}
\geometry{height=22.5cm,bottom=3.8cm}
\setlength{\parindent}{0em}
\setlength{\parskip}{0.5ex}
\begin{document}
Hello World\textlang{german}{Morgen früh: Groß oder klein?}
\end{document}
use Quiq::Sql;my $sql_o = Quiq::Sql->new('Oracle');
my $sql_m = Quiq::Sql->new('MySQL');
my $sql_l = Quiq::Sql->new('SQLite');
my $sql_p = Quiq::Sql->new('PostgreSQL');my @t_prop = (
['id',type=>'INTEGER',primaryKey=>1],
['name',type=>'STRING(30)'],
['alias',type=>'STRING(30)',notNull=>1],
);my $stmt1 = $sql_o->createTable('person', @t_prop);
my $stmt2 = $sql_m->createTable('person', @t_prop);
my $stmt3 = $sql_l->createTable('person', @t_prop);
my $stmt4 = $sql_p->createTable('person', @t_prop);say $stmt1, "\n";
say $stmt2, "\n";
say $stmt3, "\n";
say $stmt4, "\n";
Output:
CREATE TABLE person (
id NUMBER PRIMARY KEY
, name VARCHAR2(30)
, alias VARCHAR2(30) NOT NULL
)CREATE TABLE person (
id BIGINT PRIMARY KEY
, name VARCHAR(30)
, alias VARCHAR(30) NOT NULL
)
ENGINE = InnoDBCREATE TABLE person (
id INTEGER PRIMARY KEY
, name TEXT(30)
, alias TEXT(30) NOT NULL
)CREATE TABLE person (
id NUMERIC PRIMARY KEY
, name VARCHAR(30)
, alias VARCHAR(30) NOT NULL
)
Another example:
use Quiq::Sql;
my $sql = Quiq::Sql->new('SQLite');
my $stmt = $sql->select('user',login=>'mr_ng',password=>'5f4dcc3b5aa765d61d8327deb882cf99', -select=> qw/last_login_time created_at/);#SELECT
# last_login_time
# , created_at
#FROM
# user
#WHERE
# login = 'mr_ng'
# AND password = '5f4dcc3b5aa765d61d8327deb882cf99'
Combining with use Quiq::Database::Api and Quiq::Udl, one can perform useful database operations.
use Quiq::Database::Api; use Quiq::Udl; use Quiq::Sql; my $udlStr = 'dbi#mysql:assignmentdb%e78783:pa55w0rd@localhost:3306'; my $sql = Quiq::Sql->new('MySQL'); my $udlObj = Quiq::Udl->new($udlStr); my $db = Quiq::Database::Api::Dbi::Connection->new($udlObj);my $stmt = $sql->select(
'bill', billnum=>'AB23571113',
-select=> qw/userLname userFname billTotal/
);
my $row = $db->sql($stmt)->fetch;
say join " ", $row->@*;
# NG Carrie 643.4
* DBD::mysql is needed when Quiq::Database::Api is called for MySQL.
Quiq also contains some extension to Perl's idea. It names the way we write Perl hashes and arrays as "Perl Object Notation", and there are Quiq::Config utilizing it.
Quiq::Color is an utility for converting between RGB and HEX color code.
Quiq::Parallel provides a simple interface for parallel processes.
Quiq::Gimp and Quiq::ImageMagick handle the named image editor respectively.
Quiq::TreeFormatter is quite fun. It provides a staircase-like text description of trees.
use Quiq::TreeFormatter; say Quiq::TreeFormatter->new([[0,'A'],[1,'B'],[2,'C'],[3,'D']])->asText;
Output:
A
|
+--B
|
+--C
|
+--D
To Conclude...
Having more than 1MB of source code, Quiq takes time to install. It took about 20 min to install the modules on my laptop. However, I faced some difficulties during installation. GD is an unlisted but required module. More problematic for me was that I could not install one of the dependencies Filesys::SmbClient; it had been skipped by removing Smb/Client.t and using the cpan command force install Quiq.
Some modules in Quiq contains certain originality and it has been a great effort to design and write them, so I think Quiq is worth to be introduced. Other CPAN authors may get inspiration from these modules, or consider making a merge/push request to the Quiq library on GitHub.
Destination: Regexp::Assemble
Date of Latest Release: Jun 20, 2017
Distribution: Regexp-Assemble
Module version: 0.38
Main Contributors: David Landgren and Ron Savage(RSAVAGE)
Regexp::Assemble is used for combining regular expressions.
my $ra = Regexp::Assemble->new;
$ra->add('cat', 'rat');
say $ra->re;
say $ra->as_string;
# (?^:[cr]at)
# [cr]at
my $r = Regexp::Assemble->new; my @roman = qw/I II III IV V VI VII VIII IX X XI XII XIII XIV XV XVI XVII XVIII IXX XX/; $r->add(@roman); say "\$r->re:", "\n", $r->re; my $rx = $r->as_string; say "\$r->as_string:", "\n", $r->as_string; say "Matched." if "vii" =~ /$rx/i; say "This won't be printed." if "vii" =~ $r->re; # $r->re: # (?^:(?:X(?:V(?:I(?:I?I)?)?|I(?:I?I|V)?|X)?|I(?:I?I|X?X|V)?|V(?:I(?:I?I)?)?)) # $r->as_string: # (?:X(?:V(?:I(?:I?I)?)?|I(?:I?I|V)?|X)?|I(?:I?I|X?X|V)?|V(?:I(?:I?I)?)?) # Matched.
It is mainly for performance.
my $rs0 = Regexp::Assemble->new;
$rs0->add('[0-9]+');
$rs0->add('[0-9a-f]+');
$rs0->add('[0-9A-F]+');
say $rs0->re;
# (?^:(?:[0-9A-F]+|[0-9a-f]+|[0-9]+))
# though you can write [0-9A-Fa-f]+ equivalently.
It can be quite readable if your words are "similar" enough:
my $rday = Regexp::Assemble->new;
$rday->add('Wednesday');
$rday->add('Wed');
$rday->add('We');
$rday->add('W');
$rday->add('wednesday');
$rday->add('WednesdaY');
$rday->add('Wednesdy');
$rday->add('Wednseday');
$rday->add('Wedsenady');
say $rday->re;
# (?^:(?:W(?:e(?:d(?:n(?:esd(?:a[Yy]|y)|seday)|senady)?)?)?|wednesday))
Note that do not put slashes on the expressions.
my $rs = Regexp::Assemble->new;
$rs->add('m/[0-9]+/');
$rs->add('m/[0-9a-f]+/');
$rs->add('m/[0-9A-F]+/');
say $rs->re;
say "Great!?" if "A9F" =~ $rs->re;
say "Mixed feelings." if "m/A9F/" =~ $rs->re;
# (?^:m\/(?:[0-9A-F]+|[0-9a-f]+|[0-9]+)\/)
# Mixed feelings.
There are some other regular expression combinators on CPAN; one of these is Regexp::Trie.
It is generally faster than Regexp::Assemble but has fewer features.
Let's have a performance check:
# MATCHING FIRST 20 ROMAN NUMERALS # -- FROM Weekly Travelling in CPAN, Mar 07 2023 # The first author of the module, David Landgren, # has written a general example on matching Roman Numerals: # https://github.com/ronsavage/Regexp-Assemble
# /blob/master/examples/roman # The idea of the check script here is modified from that. use List::Util qw/shuffle sample any/; use Regexp::Assemble; use Regexp::Trie; use feature 'say'; sub repr { return sample int 4*rand(), shuffle('I' x (int 4*rand()), 'V', 'X'); } my $size = 1000; sub c0 { my $count = 0; for (1..$size) { my $letters = repr(); $count++ if any {$letters =~ /^$_$/} @roman; } return $count; } my $ra = Regexp::Assemble->new; $ra->anchor_line; $ra->add(@roman); my $ra_computed = $ra->re; sub c1 { my $count = 0; for (1..$size) { $count++ if repr() =~ $ra_computed; } return $count; } my $rt = Regexp::Trie->new; $rt->add($_) for @roman; my $rt_computed = $rt->regexp; sub c2 { my $count = 0; for (1..$size) { $count++ if repr() =~ /^$rt_computed$/; } return $count; } say c0()/$size; say c1()/$size; say c2()/$size; use Benchmark q/cmpthese/; cmpthese(10_000, { RAW => sub {c0}, Assemble => sub {c1}, Trie => sub {c2}, }); =pod 0.695 0.691 0.698 Rate RAW Assemble Trie RAW 43.5/s -- -92% -93% Assemble 550/s 1163% -- -18% Trie 668/s 1436% 22% --
See https://metacpan.org/pod/Regexp::Assemble and https://github.com/ronsavage/Regexp-Assemble for more details, features, and some caveats.
THE HIGHLIGHTED PERL MODULE OF WEEK 10 OF 2023: Regexp::Assemble
Computer science papers with "Perl" in the title
https://arxiv.org/search/advanced?advanced=1&terms-0-operator=AND&terms-0-term=Perl&terms-0-field=title&classification-computer_science=y (8*)
Computer science papers with "Lisp" in the title
https://arxiv.org/search/advanced?advanced=1&terms-0-operator=AND&terms-0-term=Lisp&terms-0-field=title&classification-computer_science=y (12**)
Computer science papers with "Ruby" in the title
https://arxiv.org/search/advanced?advanced=1&terms-0-operator=AND&terms-0-term=Ruby&terms-0-field=title&classification-computer_science=y (6***)
Computer science papers with "Julia" in the title
https://arxiv.org/search/advanced?advanced=&terms-0-term=Julia&terms-0-field=title&classification-computer_science=y (53 ****)
For Haskell: ~50
For Java: ~249
For Python: ~357
For Perl Data Language (PDL): 0
Besides writing quality software, organizing conference or blogging, Perl programmers with strong academic background might think of taking a new way to share their favorite functionalities in Perl to the world.
Link: arXiv.org submission guidelines
----
* 2 results are not relevant to the Perl programming language.
** 1 result is not relevant to the LISP programming language.
*** Similarly, 2 search results are irrelevant.
**** Similarly, 6 search results are irrelevant.
------
P. S.:
Many members in Perl community seem to share a concern of Perl being marginalized. As a newbie programmer and a selfish person, I also want Perl, a programming language I am "investing" in, can gain certain popularity and thus I can use Perl(Perl7?) in local job hunt in the future, hahaha. Therefore I keep an eye for the Perl's popularity discussion.
So, accumulating effort from Wednesday, today(Friday) I become a CPAN contributor!
I got a PAUSE ID 2 weeks ago. If you are also interested in the Perl ecosystem, you may consider to apply for a PAUSE ID as well.
In this blogpost, I mainly follow the instructions here:
Some contents of the PerlMonks article are largely repeated here.
This piece of PerlMonks article is already 19-year-old, but it is still valid. One of the good things of the article is that you need not install new modules or programs if you are on a *nix system.
One should have some knowledge on modules, packages and, not really necessary, object-oriented Perl ("Perl OO" in short).
Note that the Perl OO I have used in my first product is the "bless" Perl OO system, neither Moo nor Moose. Honestly I guess it is better for future maintenance if you choose to use Moo or other modern Perl OO system.
One should know how to write POD.
One should know how to use some of the Test::XXXX suite.
I had focused on typing/coding up three files:
For assistancy, I wrote a Perl script which did more time-consuming requests or for seeing the output of methods as well. Data::Dumper was a great help.
After finishing up (1) and (2), I re-organized the codes in (1) and (2) into
use Pod::Html qw/pod2html/; pod2html("", "--infile=./page.pod", "--outfile=./podpage.html");
Almostly exactly follow the instructions on the PerlMonks article by tachyon:
I moved to a directory "~/build". Then
$ h2xs -AX Math::Cryptarithm
After some trivial editing, renaming and directory operations, the structure of the "~/build" directory became
$ tree . └── Math-Cryptarithm ├── Changes ├── lib │ └── Math │ └── Cryptarithm.pm ├── Makefile.PL ├── MANIFEST ├── README └── t └── test.t
I had mainly edited Cryptarithm.pm, put the content of (1) onto it (update the version number!), and then put the POD-formatted content of (3) after __END__ in Cryptarithm.pm .
Almostly exactly follow the instructions on the PerlMonks article by tachyon:
$ tar -czf Math-Cryptarithm-0.02.tar.gz Math-Cryptarithm-0.02
$ cd ~/test $ tar -xzvf Math-Cryptarithm-0.02.tar.gz $ cd Math-Cryptarithm-0.02 $ perl Makefile.PL Checking if your kit is complete... Looks good Generating a Unix-style Makefile Writing Makefile for Math::Cryptarithm Writing MYMETA.yml and MYMETA.json $ make cp lib/Math/Cryptarithm.pm blib/lib/Math/Cryptarithm.pm Manifying 1 pod document $ make test PERL_DL_NONLAZY=1 "/home/linuxbrew/.linuxbrew/Cellar/perl/5.34.0/bin/perl" "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness(0, 'blib/lib', 'blib/arch')" t/*.t t/test.t .. ok All tests successful. Files=1, Tests=10, 34 wallclock secs ( 0.00 usr 0.02 sys + 34.47 cusr 0.00 csys = 34.49 CPU) Result: PASS $ sudo make install [sudo] password for user_name: Manifying 1 pod document Installing /home/user_name/perl5/lib/perl5/Math/Cryptarithm.pm Installing /home/user_name/perl5/man/man3/Math::Cryptarithm.3 Appending installation info to /home/user_name/perl5/lib/perl5/x86_64-linux-thread-multi/perllocal.pod
It seemed all ok.
I uploaded the .tar.gz file via https://pause.perl.org/ . Soon I got an email
"The uploaded file
Math-Cryptarithm-0.02.tar.gz
has entered CPAN as
file: $CPAN/authors/id/C/CY/myPAUSEID/Math-Cryptarithm-0.02.tar.gz
size: XX bytes
md5: XX
sha1: XX
CPAN Testers will start reporting results in an hour or so:
http://matrix.cpantesters.org/?dist=Math-Cryptarithm
Request entered by: myPAUSEID (my name)
Request entered on: Fri, 09 Jul 2021 08:09:53 GMT
Request completed: Fri, 09 Jul 2021 08:10:59 GMT
Thanks,
--
paused, v1049"
Passing the CPAN Testers is NOT necessary for the module to appear on CPAN.
Within three minutes after receiving the above email from PAUSE, I got another email from them, with the title "PAUSE indexer report myPAUSEID/Math-Cryptarithm-0.02.tar.gz":
"The following report has been written by the PAUSE namespace indexer.
...
User: ...
Distribution file: Math-Cryptarithm-0.02.tar.gz
Number of files: 6
*.pm files: 1
README: Math-Cryptarithm-0.02/README
META-File: No META.yml or META.json found
META-Parser: Parse::CPAN::Meta 1.4414
META-driven index: no
Timestamp of file: Fri Jul 9 08:10:58 2021 UTC
Time of this run: Fri Jul 9 08:12:30 2021 UTC
Status of this distro: OK
=========================
The following packages (grouped by status) have been found in the distro:
Status: Successfully indexed
============================
...
"
And I could see my module in https://metacpan.org/recent . □
]]>A coding puzzle for “The Weekly Challenge ‐ Perl & Raku” I made has been released this week!
You have 46- hours to play with it if you align with official deadline. It probably spends you 2~5 hours in this weekend. Beware! Doing the bonus part may spend you a block of extra 2 hours or more.
I wish more people will participate and show different approaches to the task. (And, may the participant give me some feedback as a puzzle creator?)
One may have advantage if s/he has played chess.
As the puzzle creator, of course I had a sketch of a solution in my mind.
Yesterday I solved (== coded) the puzzle and is blogging about it this morning. [Spoiler Alert] If you are interested in my solution... source code , blogpost
Miscellaneous:
My blog is moved to GitHub, mainly because I think soon or later I will blog on other programming languages or issues. At this moment, it has a few posts. :o)
Do tell me, if I am wrong or you strongly oppose my statements!
Well, I wake up early and get some time to blog about The Weekly Challenge again (in addition, correct some parts of my submitted code ‐ a brain with good rest well spots bugs!).
Both Perl and (guest language) Java codes for the two tasks have been done this week.
File file = new File(args[0]); Scanner sc = new Scanner(file); while (sc.hasNextLine()) { // blah blah blah }
Perl codes:
if ( checkHead(num) && checkMid(num) && num.substring(5).trim().length() == 10 && checkTail(num)) System.out.println(num); // blah blah blah private static boolean isNumeric(char ch) { if (ch >= '0' && ch <= '9') return true; else return false; }
task2_str2.csv """",",",char ok,pdl,int cu,uml,str
task2_str4.csv """",okay,char ok,pdl,int cu,uml,str
task2_ex2.csv (when the number of fields in each row is not the same) name,age,sex Mohammad,45,m,n Joe,20,m Julie,35,f Cristina,10,f
Expected Output name,Mohammad,Joe,Julie,Cristina age,45,20,35,10 sex,m,m,f,f ,n,,,
# the example from task statement name,Mohammad,Joe,Julie,Cristina age,45,20,35,10 sex,m,m,f,f # task2_str2.csv ",ok,cu ",",pdl,uml char,int,str # task2_str4.csv ",ok,cu okay,pdl,uml char,int,str # task2_ex2.csv name,Mohammad,Joe,Julie,Cristina age,45,20,35,10 sex,m,m,f,f ,n,,,
# task2_str2.csv """",ok,cu
",",pdl,uml
char,int,str
# task2_str4.csv """",ok,cu okay,pdl,uml char,int,str
Oooops... it seems that the """" for the output is better. See RFC 4180 Section 2, point 7. ...
Stay alert and healthy! □
]]>(the graph in task statement) (1) (3) ╔══════════════╗ ╔══════════════╗ ║ ║ ║ ║ ║ a ║ ║ e ║ ║ ║ (2) ║ ║ (4) ║ ┌───╫──────╫───┐ ┌───╫─────────┐ ║ │ ║ ║ │ │ ║ │ ║ │ b ║ ║ d │ │ f ║ │ ║ │ ║ ║ │ │ ║ │ ║ │ ║ ║ │ │ ║ │ ╚══════════╪═══╝ ╚═══╪══════╪═══╝ │ │ c │ │ g │ │ │ │ │ │ │ │ │ └──────────────┘ └─────────────┘
$ perl ch-2.pl Number of solutions: 8 a b c d e f g 3 7 2 1 5 4 6 4 5 3 1 6 2 7 4 7 1 3 2 6 5 5 6 2 3 1 7 4 6 4 1 5 2 3 7 6 4 5 1 2 7 3 7 2 6 1 3 5 4 7 3 2 5 1 4 6One of the possible solution(s):
**************** ****************
* * * *
* 5 * * 1 *
* * * *
* **************** ***************
* * * * * * * *
* * 6 * * 3 * * 7 * *
* * * * * * * *
* * * * * * * *
**************** **************** *
* 2 * * 4 *
* * * *
* * * *
**************** ***************
Stay alert and healthy! □
-------------------------------------
My coding momentum is a bit low.
Reflections on the codes I have written:
#095
Task 1: Palindrome Number
TMTOWTDI. On this seemly and actually simple task, I chose to compare the digit one by one.
Task 2: Demo Stack
A bit smell of laziness. I did not provide functions when stack is empty and pop() or min() is called.
#096
Task 1: Reverse Words
A lesson on extra-white space. Oppositely but as lack of caution as a sin, this morning (GMT+8) I found I have forgotten a newline for my code for #105 Task 1.
Task 2: Edit Distance
That was a standard computer science exercise. I was astounded by reading Mr Abigail's blog on the approach on saving memory space.
#097
Task 1: Caesar Cipher
Trivial.
Task 2: Binary Substrings
Seems to be weird at the first sight, but much simpler after "fourth" thought.
I wish I would have a head for calm analysis when it comes to a more time-limited situation.
#098
Task 1: Read N-characters
Trivial for Perl long-term user, but I had not known read before.
Task 2: Search Insert Position
Put a binary search tree as solution. Actually it was modified from some codes for rosalind.info .
#099
Task 1: Pattern Match
KOed by regex.
Task 2: Unique Subsequence
Quite a loaded task for me. idea->input(letter by letter);
Output: a procedural script.
#100
Task 1: Fun Time
When I was in secondary school, one of my class teachers is a strict English teacher. He said we should say "Good noon Mr Chan" if the class begins at 12:00 nn. Yeah, 12:00 nn.
Task 2: Triangle Sum
Ignoring instructions, people took the fastest way: from bottom to top traversal.
I think I am over-unpredictable on how strict I follow the examples or task instructions.
#101
My laziest week recently.
Task 1: Pack a Spiral
Back to a few years ago, I read a similar task on a competitive programming guide book. At that time I have no clues. A mark of improvement.
Task 2: origin-containing triangle
But still thanks Mr S. Little introduce some basic computer graphics task.
#102
Task 1: Rare Numbers
As said, it is faster to generate the numbers, instead of checking the natural numbers one after one... I let go of my previous experience.
Task 2: Hash-counting String
I used recursion for the task. Mr J. Smith's two elegant lines save the recursion, no wonder he gets the team title of Champion of Feb 2021 soon after he enters the team.
#103
Task 1: Chinese Zodiac
Trivial.
Task 2: What’s playing?
I got the "wrong answer" at first. Anyway, I like this task as its nature suggests me getting experience of some of the CPAN modules.
Extra:
Instead of using the Test::XXXXXX module, I wrote a short script for test. (Reinventing or whatever, I know there is a book on Perl Testing, I will check it.) This helps me get on the latest #105 -- I undergo time travel again.
Here is my testing script:
$ perl ch-1.pl 10 1048578
WARN: Recommend to take the result from Newton's method if two methods dispute WARN: N is large; probably dispute between two methods By lazy method: 4.00 By Newton's method: 4.00
$ cat tester_105-ch-1.pl
...
for (1..100) {
my $temp_N = 2 + int rand(9);
my $temp_k = rand(3000);
$data_ret{"$temp_N $temp_k"} = lazy_method($temp_N, $temp_k);
}
my $program = "perl ch-1_newton.pl"; #MODIFY FOR DIFFERENT USES
...
$ perl tester_105-ch-1.pl
...
test case parameter 10 1708.35583396219: failed
got 2.11 , expect: 2.10
...
done 100 test case(s); PASS: 99 case(s) .
Do tell me if I am wrong or you strongly oppose my statements!
Task 1 of #094 looks like a sibling of Task 1 of #092 (which Perl codes are recently reviewed, my submitted code here) and Task 2 of #094 looks like a sibling of Task 2 of #093 (where I use the array representation of binary tree, code here).
Now I was thinking of CJK characters. When comparing terms, put -CA
; and inside scripts, put use utf8; use open ':std', ':encoding(UTF-8)';
.
And my approach is similar to that of Week #092. On #092, a sub learn_pattern
produces a hash from the first parameter; and sub verify_pattern
for the second parameter returns true or false. Now, this time we face a bulk of terms, therefore we have to &collect_alphabets
: [1]
Afterwards a function for learning again
And I wrote a &compare_two_words without second thought. But it is not included in the main dish.
I group all the terms by an array of arrays and make use of a hash (%hash_compounds):
The remaining is printing result:
Here are my manual tests:
$ perl ch-1.pl "opt" "bat" "saw" "tab" "pot" "top" "was" ("opt","pot","top") ("bat","tab") ("saw","was") $ perl -CA ch-1.pl 屢敗屢戰 東南西北 屢戰屢敗 陳年 屢屢戰敗 年陳 過錯 錯過 東西南北 真善美 美善真 一二三 三二一 真善美聖 真 善 美 ("屢敗屢戰","屢戰屢敗","屢屢戰敗") ("東南西北","東西南北") ("陳年","年陳") ("過錯","錯過") ("真善美","美善真") ("一二三","三二一") ("真善美聖") ("真") ("善") ("美") $ perl ch-1.pl "x" ("x")
For the Unicode part, the reference: a stackoverflow post and a reddit post.
Write a script to represent the given binary tree as an object and flatten it to a linked list object.
Underlining is added by CY. Terms in programming can be obscure. I thought of object-oriented programming when I decided to start coding. I wrote two packages, one for linked lists, one for binary trees, in "traditional" Perl object-oriented system. I got "unblessed...". Then after a sleep, I tried Moose. The situation did not improve, Perl still gave out "unblessed..." messages. I accepted my capacity, and, looked at submitted solutions (an action I rarely do), no inspirations ‐ other submitters hadn't not touched on OO for this task. Then, unlike the previously hot celebrity in politics, I soon Accepted the Defeat ; wrote a version of script with OO linked list and array representation of binary tree; and, tweeted.
We know, tweets can be powerful! Our challenge organizer, Mohammand , replied my tweet with the word "please" and encouraged me not to give up. Initially I would like to reply with excuses. But some mysterious forces put me try harder. Although I have never written a full application by OOP (for any languages), I have heard of some OO terms. Suddenly the term "(multiple) inheritance" popped. Then I tried to write three packages, the third one specific for traversal. I used traditional Perl OO system (because I don't know how to alter attribute values in Moose). Well, my script still met hurdles - "Can't locate object method "nextnode" via package "BinaryTreeNode" ".
In the morning of Saturday, I have used two packages; put the traversal subroutine inside the binary tree package and let this package our @ISA = qw/ SLL::Node /;. Finally the script Works (and More Importantly, I Can Tweet)!
Here is the binary tree, very direct:
The linked list used differ from codes written three months ago (the post here) just by an additional one-line method ‐
For the traversal (inside the package BinaryTreeNode
):
For the printing (similar to the post in the three months ago):
Full code on GitHub: link (with Java solution for task 2) □
Stay healthy! (written on 9th Jan 2021 (probably a remarkable day in the 21st century history) night, Hong Kong Time Zone).
Thank you Ben again. Haha I don't mind about the tone; every stuff in a knowledge domain for a beginner is fresh-new.
]]>