Statistics about COVID-19 in Command Line with Perl
As a regular Linux user, I’m aware of the fact that command line can save me a lot of time. In the light of the current COVID-19 pandemic, I’d like to see some statistics printed out in my terminal window. I’ll build this utility in Perl, it will consist of two parts - a module for getting the data and a frontend part that will pretty print those data into a terminal window.
API
A big part of my success in writing this utility is finding a good API where I can get the desired data. That takes a little bit of googling around.
One API I found sufficient is this wuhan-coronavirus-api. The data source seems reliable (Johns Hopkins CSSE), it’s updated regularly (couple of times a day), and it’s pretty easy to use (open API with no need to register anywhere). So, I decided to choose this one, to be more precise, I’ll be using /jhu-edu/latest
endpoint.
API data
The API returns data in the following format:
[
{
"provincestate": "",
"countryregion": "Thailand",
"lastupdate": "2020-03-21T17:42:00.010Z",
"location": { "lat": 15, "lng": 101 },
"countrycode":{ "iso2": "TH", "iso3": "THA"},
"confirmed": 322,
"deaths": 1,
"recovered": 42
},
{ ... }
]
The only problem is that the object doesn’t necessarily have to have all these attributes - provincestate
could be missing, countrycode
could be missing (e.g. it’s missing for the Czechia for some reason), and even the statistics of people affected could be missing. So it’s important to take it into account.
The API also allows to use a query parameter onlyCountries
which is a boolean value (true or false). However, I aggregate the data on my side since I noticed this param too late and I don’t want to rebuild my module now.
Perl
Let’s think about what I want from my little utility in the first place:
- I’d like to get statistics of total cases, number of recovered people, number of deaths, and what was the last data update
- I’d like to get statistics for either one country, a few countries, or all countries available
- in the frontend part, I’d like to pretty print it in some sort of table where values are aligned
The main flow could look something like this:
my $corona = Corona::Statistics->new();
# getting data
$corona->get_all_stats();
# filtering only countries I want
my $selected_countries = $corona->get_country_stats(\@countries);
die "No country found\n" if not @$selected_countries;
# pretty print everything to STDOUT
pretty_print();
With that said, let’s build the module I call Corona
.
First the constructor:
sub new {
my $class = shift;
my $iso2_filename = shift;
my $iso3_filename = shift;
# because ConfigReader::Simple can't interpret '~' in path
$iso2_filename =~ s/^~/$ENV{HOME}/ if $iso2_filename and $ENV{HOME};
$iso3_filename =~ s/^~/$ENV{HOME}/ if $iso3_filename and $ENV{HOME};
return bless { "iso2" => $iso2_filename,
"iso3" => $iso3_filename, countries => {}
} => $class;
}
Then the subroutine for getting data from the endpoint. This block will use module Net::HTTPS
which you most likely need to install with your package manager or using CPAN.
sub get_all_stats {
my $self = shift;
# if I already have data, don't call the endpoint again
return DATA_IS_PRESENT if keys %{ $self->{countries} };
# new request
my $req = Net::HTTPS->new(Host => BASE_URL)
or croak __PACKAGE__ . "::get_data(): Cannot connect to the server\n";
$req->write_request(GET => COUNTRY_STATS, 'Accept' => 'application/json');
croak __PACKAGE__ . "::get_data(): Cannot get answer from the server\n"
if not $req->read_response_headers;
my $response_body = "";
# read response body as long as there's some data
while (1) {
my $buf;
my $body = $req->read_entity_body($buf, 1024);
croak __PACKAGE__ . "get_data(): Error reading the response\n"
if not defined $body;
last if not $body;
$response_body .= $buf;
}
# parse the response body
$self->_parse_response_body(\$response_body);
return SUCCESS;
}
The subroutine _parse_response_body()
is the key, so let’s show its content here as well:
sub _parse_response_body {
my $self = shift;
my $response_body_ref = shift;
# http response is a json
my $corona_stats = parse_json($$response_body_ref);
# also try to look up country codes in dot files in $HOME
my $iso2_file = ConfigReader::Simple->new($self->{iso2});
my $iso3_file = ConfigReader::Simple->new($self->{iso3});
foreach my $country (@$corona_stats) {
my $countryname = lc $country->{countryregion};
$self->{countries}->{$countryname}->{countryname}
= $country->{countryregion};
$self->{countries}->{$countryname}->{lastupdate}
= $country->{lastupdate};
$self->{countries}->{$countryname}->{confirmed}
+= $country->{confirmed}
if defined $country->{confirmed};
$self->{countries}->{$countryname}->{recovered}
+= $country->{recovered}
if defined $country->{recovered};
$self->{countries}->{$countryname}->{deaths}
+= $country->{deaths}
if defined $country->{deaths};
if (defined $country->{countrycode}->{iso2}) {
$self->{countries}->{$countryname}->{iso2}
= lc $country->{countrycode}->{iso2};
}
# try to look up country codes in config files
else {
$self->{countries}->{$countryname}->{iso2}
= lc $iso2_file->get($countryname)
if $iso2_file->get($countryname) and $iso2_file;
}
if (defined $country->{countrycode}->{iso3}) {
$self->{countries}->{$countryname}->{iso3}
= lc $country->{countrycode}->{iso3};
}
# try to look up country codes in config files
else {
$self->{countries}->{$countryname}->{iso3}
= lc $iso3_file->get($countryname)
if $iso3_file->get($countryname) and $iso3_file;
}
}
}
As I mentioned before, I need to make sure attributes exist in the objects returned from the API, otherwise I need to go on.
Now I have all the data in a structure that Perl understands, so the next step would be to get only those country statistics the user asked for on the command line. For that, I’ll use the Getopt::Long
module:
# get command line switches
my $result = GetOptions(
'help|h' => \ my $requested_help,
'country|c=s@' => \ my @selected_countries_by_user
);
I’ll also make the list given by the user unique (and lowercase), so no value will be there more times:
use List::MoreUtils qw(uniq);
@selected_countries_by_user = uniq @selected_countries_by_user;
# make the values lower case
@selected_countries_by_user
= map { tr/[A-Z]/[a-z]/; $_ } @selected_countries_by_user;
Module List::MoreUtils
will likely have to be installed as well.
Finally, I have everything needed for building the subroutine get_country_data()
:
sub get_country_stats {
my $self = shift;
my $user_given_countries_ref = shift;
my @selected_countries = ();
# if user requested all countries
if ($user_given_countries_ref->[0] =~ /^all$/i) {
my @all_countries = keys %{ $self->{countries} };
$user_given_countries_ref = \@all_countries;
}
foreach my $country (@$user_given_countries_ref) {
# find by key first
if ($self->{countries}->{$country}) {
push @selected_countries, $self->{countries}->{$country};
}
# find by iso2 or iso3
else {
my $country_by_iso = $self->_get_country_data_by_iso($country);
if ($country_by_iso) {
push @selected_countries, $country_by_iso;
}
}
}
return \@selected_countries;
}
It will return a reference to a list of objects (statistics). I primarily search by country names which is a key in the object, but the user might also specify iso2 or iso3 country names or combine all of these, therefore I need to search by iso2 or iso3 as well:
sub _get_country_data_by_iso {
my $self = shift;
my $iso = shift;
foreach my $country (keys %{ $self->{countries} }) {
# skip if a country doesn't have iso2 or iso3 attributes
next if not $self->{countries}->{$country}->{iso2};
next if not $self->{countries}->{$country}->{iso3};
# return a reference to a particular object found by iso2 or iso3
return $self->{countries}->{$country}
if $self->{countries}->{$country}->{iso2} eq $iso;
return $self->{countries}->{$country}
if $self->{countries}->{$country}->{iso3} eq $iso;
}
return FAILURE;
}
That’s all, the whole module looks like this:
package Corona::Statistics;
use strict;
use warnings;
use Net::HTTPS;
use Carp qw(croak);
use JSON::Parse qw(parse_json);
use ConfigReader::Simple qw(get);
our $VERSION = '1.03';
use constant BASE_URL => "wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai";
use constant COUNTRY_STATS => "/jhu-edu/latest";
use constant SUCCESS => 1;
use constant FAILURE => 0;
use constant DATA_IS_PRESENT => 0;
sub new {
my $class = shift;
my $iso2_filename = shift;
my $iso3_filename = shift;
# because ConfigReader::Simple can't interpret '~' in path
$iso2_filename =~ s/^~/$ENV{HOME}/ if $iso2_filename and $ENV{HOME};
$iso3_filename =~ s/^~/$ENV{HOME}/ if $iso3_filename and $ENV{HOME};
return bless { "iso2" => $iso2_filename,
"iso3" => $iso3_filename, countries => {}
} => $class;
}
# gets data from the endpoint
# calls _parse_response_body() for parsing of the data
# returns 1 if there's no error
sub get_all_stats {
my $self = shift;
# if I already have data, don't call the endpoint again
return DATA_IS_PRESENT if keys %{ $self->{countries} };
# new request
my $req = Net::HTTPS->new(Host => BASE_URL)
or croak __PACKAGE__ . "::get_data(): Cannot connect to the server\n";
$req->write_request(GET => COUNTRY_STATS, 'Accept' => 'application/json');
croak __PACKAGE__ . "::get_data(): Cannot get answer from the server\n"
if not $req->read_response_headers;
my $response_body = "";
# read response body as long as there's some data
while (1) {
my $buf;
my $body = $req->read_entity_body($buf, 1024);
croak __PACKAGE__ . "get_data(): Error reading the response\n"
if not defined $body;
last if not $body;
$response_body .= $buf;
}
# parse the response body
$self->_parse_response_body(\$response_body);
return SUCCESS;
}
# parses a http body response and fills the current object with data
# confirmed, recovered, deaths, countrycode attributes might be missing in the object
# countryregion is not unique in the object
# [
# {
# "provincestate":"",
# "countryregion":"Thailand",
# "lastupdate":"2020-03-21T17:42:00.010Z",
# "location":{"lat":15,"lng":101},
# "countrycode":{"iso2":"TH","iso3":"THA"},
# "confirmed":322,
# "deaths":1,
# "recovered":42
# },
# { ... }
#]
sub _parse_response_body {
my $self = shift;
my $response_body_ref = shift;
# http response is a json
my $corona_stats = parse_json($$response_body_ref);
# also try to look up country codes in dot files in $HOME
my $iso2_file = ConfigReader::Simple->new($self->{iso2});
my $iso3_file = ConfigReader::Simple->new($self->{iso3});
foreach my $country (@$corona_stats) {
my $countryname = lc $country->{countryregion};
$self->{countries}->{$countryname}->{countryname}
= $country->{countryregion};
$self->{countries}->{$countryname}->{lastupdate}
= $country->{lastupdate};
$self->{countries}->{$countryname}->{confirmed}
+= $country->{confirmed}
if defined $country->{confirmed};
$self->{countries}->{$countryname}->{recovered}
+= $country->{recovered}
if defined $country->{recovered};
$self->{countries}->{$countryname}->{deaths}
+= $country->{deaths}
if defined $country->{deaths};
if (defined $country->{countrycode}->{iso2}) {
$self->{countries}->{$countryname}->{iso2}
= lc $country->{countrycode}->{iso2};
}
# try to look up country codes in config files
else {
$self->{countries}->{$countryname}->{iso2}
= lc $iso2_file->get($countryname)
if $iso2_file->get($countryname) and $iso2_file;
}
if (defined $country->{countrycode}->{iso3}) {
$self->{countries}->{$countryname}->{iso3}
= lc $country->{countrycode}->{iso3};
}
# try to look up country codes in config files
else {
$self->{countries}->{$countryname}->{iso3}
= lc $iso3_file->get($countryname)
if $iso3_file->get($countryname) and $iso3_file;
}
}
}
# selects some country data out of all country data returned by the endpoint
# returns a reference to a list of objects
sub get_country_stats {
my $self = shift;
my $user_given_countries_ref = shift;
my @selected_countries = ();
# if user requested all countries
if ($user_given_countries_ref->[0] =~ /^all$/i) {
my @all_countries = keys %{ $self->{countries} };
$user_given_countries_ref = \@all_countries;
}
foreach my $country (@$user_given_countries_ref) {
# find by key first
if ($self->{countries}->{$country}) {
push @selected_countries, $self->{countries}->{$country};
}
# find by iso2 or iso3
else {
my $country_by_iso = $self->_get_country_data_by_iso($country);
if ($country_by_iso) {
push @selected_countries, $country_by_iso;
}
}
}
return \@selected_countries;
}
# user can add iso2 or iso3 arguments on the command line,
# but these are not keys of the hash, so try to look for
# iso2 or iso3 in the objects
# returns a ref to an object or 0 if nothing is found
sub _get_country_data_by_iso {
my $self = shift;
my $iso = shift;
foreach my $country (keys %{ $self->{countries} }) {
# skip if a country doesn't have iso2 or iso3 attributes
next if not $self->{countries}->{$country}->{iso2};
next if not $self->{countries}->{$country}->{iso3};
# return a reference to a particular object found by iso2 or iso3
return $self->{countries}->{$country}
if $self->{countries}->{$country}->{iso2} eq $iso;
return $self->{countries}->{$country}
if $self->{countries}->{$country}->{iso3} eq $iso;
}
return FAILURE;
}
1;
__END__
=head1 NAME
Corona::Statistics - get statistics about COVID-19
=head1 SYNOPSIS
# initialize
my $corona = Corona::Statistics->new();
# get all available data about COVID-19 in different countries
$corona->get_all_stats();
# if called again, the endpoint won't be called because you already have
# data, so if you need a new set of data, create a new object
$corona->get_all_stats();
# get data for only those countries you're interested in
# pass a ref to a list of country names (or iso2 or iso3 abbreviations)
my $selected_countries
= $corona->get_country_stats(\@selected_countries_by_user);
# if the list is empty, nothing has been found
# otherwise a list of objects is returned:
# [
# {
# 'iso3' => 'EST',
# 'recovered' => 1,
# 'countryname' => 'Estonia',
# 'lastupdate' => '2020-03-22T10:42:00.003Z',
# 'iso2' => 'EE',
# 'confirmed' => 306,
# 'deaths' => 0
# },
# { ... }
# ]
die "No country found\n" if not @$selected_countries;
=head1 DESCRIPTION
Corona::Statistics module provides data about COVID-19 pandemic for different
countries.
The following endpoint is used for getting the data:
https://wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai/jhu-edu/latest
More about the endpoint could be found here:
https://ainize.ai/laeyoung/wuhan-coronavirus-api
=head1 DEPENDENCIES
The following Perl modules are required for this Corona module to work:
=over 8
=item Net::HTTPS
For getting data from endpoint in get_all_stats().
=item JSON::Parse
For parsing a http response body in _parse_response_body().
=item ConfigReader::Simple
Reading country codes from a config file.
=item Carp
For more informative error messages.
=back
=head1 AUTHOR
Pavel Saman
=head1 LICENSE
This library is free software; you may redistribute it and/or modify it under the same terms as Perl itself.
=cut
You can find it on github here.
Back to the frontend part. So far, I’ve built command line switches using the Getopt::Long
module and the part where I’m making the given list of specified countries unique. Let’s see the rest:
#!/usr/bin/env perl
# frontend part for Corona::Statistics module
# modules Getopt::Long, Corona::Statistics,
# List::MoreUtils, and Term::Table required
use strict;
use warnings;
use Getopt::Long;
use Corona::Statistics 1.03;
use feature 'say';
# transforms a json to a list needed for Term::Table
sub put_country_stats_to_list {
my $all_country_stats_ref = shift;
# build a list from list of jsons
my @country_stats_list = ();
foreach my $country (@$all_country_stats_ref) {
my $country_stats = [
$country->{countryname}, $country->{confirmed},
$country->{recovered}, $country->{deaths},
$country->{lastupdate}
]
;
push @country_stats_list, $country_stats;
}
return \@country_stats_list;
}
# gets number of columns for the current terminal window
sub get_number_of_columns {
return undef if $^O =~ /Win32$/;
open COL, "tput cols|";
my $number_of_columns_in_terminal = <COL>;
close COL;
return $number_of_columns_in_terminal;
}
sub print_help {
print <<'END_MSG';
corona.pl -c|--country Estonia [-c|--country Italy ...]
corona.pl -c|--country all|ALL
Prints COVID-19 statistics for specified countries.
Prints all country statistics if -c|--coutry all is given.
Print this help with -h|--help option.
END_MSG
}
###############################################################################
# get command line switches
my $result = GetOptions(
'help|h' => \ my $requested_help,
'country|c=s@' => \ my @selected_countries_by_user
);
# only print help
if ($requested_help or not @selected_countries_by_user) {
print_help();
exit 0;
}
# corona object
my $corona = Corona::Statistics->new("~/.2country", "~/.3country");
# get all data; it will die inside get_data() if the call is unsuccessful
$corona->get_all_stats();
# make the list of countries unique
use List::MoreUtils qw(uniq);
@selected_countries_by_user = uniq @selected_countries_by_user;
# make the values lower case
@selected_countries_by_user
= map { tr/[A-Z]/[a-z]/; $_ } @selected_countries_by_user;
# get desired countries
my $selected_countries
= $corona->get_country_stats(\@selected_countries_by_user);
die "No country found\n" if not @$selected_countries;
# pretty print everything to STDOUT
use Term::Table;
my $corona_stats_table = Term::Table->new(
max_width => get_number_of_columns() || 80,
pad => 4,
allow_overflow => 0,
collapse => 1,
header => ['country', 'total cases',
'recovered', 'deaths', 'last updated'],
rows => put_country_stats_to_list($selected_countries),
)
;
say $_ for $corona_stats_table->render;
exit 0;
For pretty printing, I’m using module Term::Table
that gets a list reference as data for the table rows. That’s a little trouble since my Corona module returns data as a reference to a list of objects. Therefore, I need to transform the data into a desired format for the Term:Table
module:
sub put_country_stats_to_list {
my $all_country_stats_ref = shift;
# build a list from list of jsons
my @country_stats_list = ();
foreach my $country (@$all_country_stats_ref) {
my $country_stats = [
$country->{countryname}, $country->{confirmed},
$country->{recovered}, $country->{deaths},
$country->{lastupdate}
]
;
push @country_stats_list, $country_stats;
}
return \@country_stats_list;
}
I’m also making the table wider if it’s possible:
sub get_number_of_columns {
return undef if $^O =~ /Win32$/;
open COL, "tput cols|";
my $number_of_columns_in_terminal = <COL>;
close COL;
return $number_of_columns_in_terminal;
}
and
my $corona_stats_table = Term::Table->new(
max_width => get_number_of_columns() || 80,
pad => 4,
allow_overflow => 0,
collapse => 1,
header => ['country', 'total cases',
'recovered', 'deaths', 'last updated'],
rows => put_country_stats_to_list($selected_countries),
)
;
And that’s basically all.
How to use it
You can put the script into your $PATH
, you also need to tell Perl where to find the module, so you likely want to adjust PERL5LIB
environment variable, which will add items to @INC
.
After that, you just type:
$ corona.pl -c Czechia
+---------+-------------+-----------+--------+--------------------------+
| country | total cases | recovered | deaths | last updated |
+---------+-------------+-----------+--------+--------------------------+
| Czechia | 995 | 6 | 0 | 2020-03-22T13:42:00.004Z |
+---------+-------------+-----------+--------+--------------------------+
or you can specify more countries, you can also mix country names, iso2, and iso3:
$ corona.pl -c czechia -c rus -c IT
+---------+-------------+-----------+--------+--------------------------+
| country | total cases | recovered | deaths | last updated |
+---------+-------------+-----------+--------+--------------------------+
| Czechia | 995 | 6 | 0 | 2020-03-22T13:42:00.007Z |
| Russia | 306 | 12 | 1 | 2020-03-22T13:42:00.007Z |
| Italy | 53578 | 6072 | 4825 | 2020-03-22T13:42:00.007Z |
+---------+-------------+-----------+--------+--------------------------+
or you can print a full list of countries:
$ corona.pl -c all
+----------------------------------+-------------+-----------+--------+--------------------------+
| country | total cases | recovered | deaths | last updated |
+----------------------------------+-------------+-----------+--------+--------------------------+
| Uganda | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Congo (Brazzaville) | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Estonia | 306 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Kazakhstan | 53 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Brunei | 83 | 2 | 0 | 2020-03-22T13:42:00.007Z |
| Ireland | 785 | 5 | 3 | 2020-03-22T13:42:00.007Z |
| Malaysia | 1183 | 114 | 4 | 2020-03-22T13:42:00.007Z |
| Nepal | 1 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Niger | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Kenya | 7 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Iran | 20610 | 7635 | 1556 | 2020-03-22T13:42:00.007Z |
| Philippines | 307 | 13 | 19 | 2020-03-22T13:42:00.007Z |
| Bulgaria | 163 | 3 | 3 | 2020-03-22T13:42:00.007Z |
| Paraguay | 18 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| France | 14431 | 12 | 562 | 2020-03-22T13:42:00.007Z |
| Armenia | 160 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Uruguay | 110 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Eritrea | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Cote d\'Ivoire | 14 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Benin | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Chile | 537 | 6 | 0 | 2020-03-22T13:42:00.007Z |
| Senegal | 47 | 5 | 0 | 2020-03-22T13:42:00.007Z |
| Maldives | 13 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Norway | 2118 | 1 | 7 | 2020-03-22T13:42:00.007Z |
| Netherlands | 3640 | 2 | 137 | 2020-03-22T13:42:00.007Z |
| Montenegro | 14 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Mauritius | 14 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Greece | 530 | 19 | 13 | 2020-03-22T13:42:00.007Z |
| Holy See | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Kuwait | 176 | 27 | 0 | 2020-03-22T13:42:00.007Z |
| Mauritania | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Honduras | 24 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Andorra | 88 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Latvia | 124 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| United Kingdom | 5067 | 67 | 234 | 2020-03-22T13:42:00.007Z |
| Costa Rica | 117 | 2 | 2 | 2020-03-22T13:42:00.007Z |
| Pakistan | 730 | 13 | 3 | 2020-03-22T13:42:00.007Z |
| San Marino | 144 | 4 | 20 | 2020-03-22T13:42:00.007Z |
| Saint Vincent and the Grenadines | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Liberia | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Cyprus | 84 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Chad | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Zimbabwe | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Japan | 1007 | 232 | 35 | 2020-03-22T13:42:00.007Z |
| Gabon | 4 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Czechia | 995 | 6 | 0 | 2020-03-22T13:42:00.007Z |
| Cruise Ship | 712 | 325 | 8 | 2020-03-22T13:42:00.007Z |
| Iraq | 214 | 51 | 17 | 2020-03-22T13:42:00.007Z |
| China | 81305 | 71857 | 3259 | 2020-03-22T13:42:00.007Z |
| Egypt | 294 | 41 | 10 | 2020-03-22T13:42:00.007Z |
| Seychelles | 7 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Finland | 523 | 10 | 1 | 2020-03-22T13:42:00.007Z |
| Lithuania | 83 | 1 | 1 | 2020-03-22T13:42:00.007Z |
| Bahrain | 305 | 125 | 1 | 2020-03-22T13:42:00.007Z |
| Switzerland | 6575 | 15 | 75 | 2020-03-22T13:42:00.007Z |
| Slovenia | 383 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| North Macedonia | 85 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Equatorial Guinea | 6 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Namibia | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Zambia | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Lebanon | 187 | 4 | 4 | 2020-03-22T13:42:00.007Z |
| Venezuela | 70 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Barbados | 6 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Serbia | 171 | 1 | 1 | 2020-03-22T13:42:00.007Z |
| Sri Lanka | 77 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Belgium | 2815 | 263 | 67 | 2020-03-22T13:42:00.007Z |
| Algeria | 139 | 32 | 15 | 2020-03-22T13:42:00.007Z |
| Russia | 306 | 12 | 1 | 2020-03-22T13:42:00.007Z |
| Romania | 367 | 52 | 0 | 2020-03-22T13:42:00.007Z |
| Colombia | 196 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Saint Lucia | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Sweden | 1763 | 16 | 20 | 2020-03-22T13:42:00.007Z |
| Congo (Kinshasa) | 23 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Luxembourg | 670 | 0 | 8 | 2020-03-22T13:42:00.007Z |
| Dominican Republic | 112 | 0 | 2 | 2020-03-22T13:42:00.007Z |
| Cabo Verde | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Canada | 1278 | 10 | 19 | 2020-03-22T13:42:00.007Z |
| US | 25417 | 0 | 307 | 2020-03-22T13:42:00.007Z |
| Kyrgyzstan | 14 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Central African Republic | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Trinidad and Tobago | 49 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Kosovo | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Ghana | 19 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Albania | 76 | 2 | 2 | 2020-03-22T13:42:00.007Z |
| Madagascar | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Brazil | 1021 | 2 | 15 | 2020-03-22T13:42:00.007Z |
| Azerbaijan | 53 | 11 | 1 | 2020-03-22T13:42:00.007Z |
| Liechtenstein | 37 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Bhutan | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Argentina | 158 | 3 | 4 | 2020-03-22T13:42:00.007Z |
| Martinique | 32 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Bosnia and Herzegovina | 93 | 2 | 1 | 2020-03-22T13:42:00.007Z |
| Hungary | 103 | 7 | 4 | 2020-03-22T13:42:00.007Z |
| Bangladesh | 25 | 3 | 2 | 2020-03-22T13:42:00.007Z |
| Slovakia | 178 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Mexico | 203 | 4 | 2 | 2020-03-22T13:42:00.007Z |
| Panama | 200 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Malta | 73 | 2 | 0 | 2020-03-22T13:42:00.007Z |
| Cuba | 21 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Korea, South | 8799 | 1540 | 102 | 2020-03-22T13:42:00.007Z |
| Israel | 883 | 36 | 1 | 2020-03-22T13:42:00.007Z |
| Tunisia | 60 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Angola | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Saudi Arabia | 392 | 16 | 0 | 2020-03-22T13:42:00.007Z |
| Taiwan* | 153 | 28 | 2 | 2020-03-22T13:42:00.007Z |
| Vietnam | 94 | 17 | 0 | 2020-03-22T13:42:00.007Z |
| South Africa | 240 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Turkey | 670 | 0 | 9 | 2020-03-22T13:42:00.007Z |
| Portugal | 1280 | 5 | 12 | 2020-03-22T13:42:00.007Z |
| Ukraine | 47 | 1 | 3 | 2020-03-22T13:42:00.007Z |
| India | 330 | 23 | 4 | 2020-03-22T13:42:00.007Z |
| Uzbekistan | 43 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| New Zealand | 52 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Cambodia | 53 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Gambia, The | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Oman | 52 | 12 | 0 | 2020-03-22T13:42:00.007Z |
| East Timor | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Haiti | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| United Arab Emirates | 153 | 38 | 2 | 2020-03-22T13:42:00.007Z |
| Morocco | 96 | 3 | 3 | 2020-03-22T13:42:00.007Z |
| Ethiopia | 9 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Eswatini | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Nigeria | 22 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Poland | 536 | 1 | 5 | 2020-03-22T13:42:00.007Z |
| Cameroon | 27 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Belarus | 76 | 15 | 0 | 2020-03-22T13:42:00.007Z |
| Iceland | 473 | 22 | 1 | 2020-03-22T13:42:00.007Z |
| Guatemala | 17 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Burkina Faso | 64 | 5 | 2 | 2020-03-22T13:42:00.007Z |
| Guinea | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Bahamas, The | 4 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Sudan | 2 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Togo | 16 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Qatar | 481 | 27 | 0 | 2020-03-22T13:42:00.007Z |
| Nicaragua | 2 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Cape Verde | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Somalia | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Moldova | 80 | 1 | 1 | 2020-03-22T13:42:00.007Z |
| Indonesia | 450 | 15 | 38 | 2020-03-22T13:42:00.007Z |
| Australia | 1071 | 26 | 7 | 2020-03-22T13:42:00.007Z |
| Papua New Guinea | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Rwanda | 17 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Spain | 25374 | 2125 | 1375 | 2020-03-22T13:42:00.007Z |
| Suriname | 4 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Bolivia | 19 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Ecuador | 506 | 3 | 7 | 2020-03-22T13:42:00.007Z |
| Singapore | 432 | 140 | 2 | 2020-03-22T13:42:00.007Z |
| Guyana | 7 | 0 | 1 | 2020-03-22T13:42:00.007Z |
| Tanzania | 6 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Peru | 318 | 1 | 5 | 2020-03-22T13:42:00.007Z |
| Jamaica | 16 | 2 | 1 | 2020-03-22T13:42:00.007Z |
| El Salvador | 3 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Germany | 22213 | 233 | 84 | 2020-03-22T13:42:00.007Z |
| Thailand | 411 | 42 | 1 | 2020-03-22T13:42:00.007Z |
| Italy | 53578 | 6072 | 4825 | 2020-03-22T13:42:00.007Z |
| Monaco | 11 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Fiji | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Austria | 2814 | 9 | 8 | 2020-03-22T13:42:00.007Z |
| Antigua and Barbuda | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Jordan | 85 | 1 | 0 | 2020-03-22T13:42:00.007Z |
| Djibouti | 1 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Croatia | 206 | 5 | 1 | 2020-03-22T13:42:00.007Z |
| Denmark | 1420 | 1 | 13 | 2020-03-22T13:42:00.007Z |
| Mongolia | 10 | 0 | 0 | 2020-03-22T13:42:00.007Z |
| Afghanistan | 24 | 1 | 0 | 2020-03-22T13:42:00.007Z |
+----------------------------------+-------------+-----------+--------+--------------------------+
You can print help with -h
or --help
switch.
The whole module and the frontend part could be found on github here.
EDIT:
Based on my latest learnings of Perl, I’ve updated the code, so it should be free of some obvious mistakes I made back then. Some things I updated are:
- better names of variables, etc. based Perl Best Practises book, chapter 2
- renamed
Corona::Corona
toCorona::Statistics
, tha former didn’t make much sense and it crated more problems with e.g.$VERSION
variable - started using
Carp
module andcroak()
function, this is purely because I wanted to try it out, but there’s not much changed in terms of functionality, you just get a bit more info in case of problems - it can read config files with key value pairs of iso2 and iso3 country codes, the config files could be passed into the constructor
- started using
our $VERSION
syntax . changedmax_width => get_number_of_columns() // 80
tomax_width => get_number_of_columns() || 80
because I want it to be 80 even for e.g. value 0 returned (from whatever reason it might happen)