line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# Weather::GHCN::Common.pm - common functions for GHCN scripts and modules
|
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
## no critic (Documentation::RequirePodAtEnd)
|
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 NAME
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
Weather::GHCN::Common - common functions for GHCN scripts and modules
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
=head1 VERSION |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
version v0.0.010 |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
use Weather::GHCN::Common qw(:all);
|
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
The B module provides functions that are used in more
|
21
|
|
|
|
|
|
|
than one GHCN module, or that may be useful in application scripts;
|
22
|
|
|
|
|
|
|
e.g. rng_valid() to validate number ranges that might be provided
|
23
|
|
|
|
|
|
|
to a script using command line arguments.
|
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
The module is primarily for use by modules Weather::GHCN::Fetch, Weather::GHCN::Options,
|
26
|
|
|
|
|
|
|
Weather::GHCN::Station, and Weather::GHCN::StationTable.
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=cut
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
## no critic [ValuesAndExpressions::ProhibitVersionStrings]
|
31
|
|
|
|
|
|
|
## no critic [TestingAndDebugging::RequireUseWarnings]
|
32
|
|
|
|
|
|
|
## no critic [ProhibitSubroutinePrototypes]
|
33
|
|
|
|
|
|
|
## no critic [References::ProhibitDoubleSigils]
|
34
|
|
|
|
|
|
|
|
35
|
7
|
|
|
7
|
|
110147
|
use v5.18; # minimum for Object::Pad
|
|
7
|
|
|
|
|
30
|
|
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
package Weather::GHCN::Common;
|
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
our $VERSION = 'v0.0.010'; |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
|
42
|
7
|
|
|
7
|
|
45
|
use feature 'signatures';
|
|
7
|
|
|
|
|
13
|
|
|
7
|
|
|
|
|
681
|
|
43
|
7
|
|
|
7
|
|
44
|
no warnings 'experimental::signatures';
|
|
7
|
|
|
|
|
19
|
|
|
7
|
|
|
|
|
292
|
|
44
|
|
|
|
|
|
|
|
45
|
7
|
|
|
7
|
|
40
|
use Exporter;
|
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
292
|
|
46
|
7
|
|
|
7
|
|
1839
|
use parent 'Exporter';
|
|
7
|
|
|
|
|
1197
|
|
|
7
|
|
|
|
|
47
|
|
47
|
|
|
|
|
|
|
|
48
|
7
|
|
|
7
|
|
541
|
use Carp qw(croak);
|
|
7
|
|
|
|
|
15
|
|
|
7
|
|
|
|
|
322
|
|
49
|
7
|
|
|
7
|
|
486
|
use Const::Fast;
|
|
7
|
|
|
|
|
2699
|
|
|
7
|
|
|
|
|
51
|
|
50
|
7
|
|
|
7
|
|
1916
|
use Try::Tiny;
|
|
7
|
|
|
|
|
6237
|
|
|
7
|
|
|
|
|
419
|
|
51
|
7
|
|
|
7
|
|
3418
|
use Set::IntSpan::Fast;
|
|
7
|
|
|
|
|
42877
|
|
|
7
|
|
|
|
|
6263
|
|
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
const my $EMPTY => q();
|
54
|
|
|
|
|
|
|
const my $TAB => qq(\t);
|
55
|
|
|
|
|
|
|
const my $NL => qq(\n);
|
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
const my $RANGE_RE => qr{ \d+ (?: [-] \d+ )? }xms;
|
58
|
|
|
|
|
|
|
const my $RANGE_LIST_RE => qr{ \A $RANGE_RE (?: [,] $RANGE_RE )* \Z }xms;
|
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
our %EXPORT_TAGS = ( 'all' => [ qw(
|
61
|
|
|
|
|
|
|
commify
|
62
|
|
|
|
|
|
|
np_trim
|
63
|
|
|
|
|
|
|
rng_new
|
64
|
|
|
|
|
|
|
rng_valid
|
65
|
|
|
|
|
|
|
rng_within
|
66
|
|
|
|
|
|
|
tsv
|
67
|
|
|
|
|
|
|
iso_date_time
|
68
|
|
|
|
|
|
|
) ] );
|
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
|
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=head1 FUNCTIONS
|
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
=head2 commify($number)
|
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
Insert commas into a number so that digits are grouped in threes;
|
78
|
|
|
|
|
|
|
e.g. 12345 becomes 12,345.
|
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
The argument can be a number or a string of digits, with or without
|
81
|
|
|
|
|
|
|
a decimal. Digits after a decimal are unaffected.
|
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
=cut
|
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
# insert commas into a number
|
86
|
37
|
|
|
37
|
1
|
1819
|
sub commify ($arg) {
|
|
37
|
|
|
|
|
56
|
|
|
37
|
|
|
|
|
58
|
|
87
|
|
|
|
|
|
|
|
88
|
37
|
|
100
|
|
|
85
|
$arg //= q();
|
89
|
|
|
|
|
|
|
|
90
|
37
|
|
|
|
|
98
|
my $text = reverse $arg;
|
91
|
|
|
|
|
|
|
|
92
|
37
|
|
|
|
|
230
|
$text =~ s{ (\d\d\d) (?=\d) (?! \d* [.] ) }{$1,}xmsg;
|
93
|
|
|
|
|
|
|
|
94
|
37
|
|
|
|
|
182
|
return scalar reverse $text;
|
95
|
|
|
|
|
|
|
}
|
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=head2 rng_new(@args)
|
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
Wrapper for Set::IntSpan::Fast->new(), it provides a shorter name
|
100
|
|
|
|
|
|
|
as well as:
|
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
- allowing an undef $range to create an empty set
|
103
|
|
|
|
|
|
|
- croaking if new() fails for any reason
|
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
The arguments to rng_new can consist of a range string (e.g. '1-5,12')
|
106
|
|
|
|
|
|
|
or a perl list of numbers (e.g. 1,7,12,20..25) or a mix of both.
|
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
=cut
|
109
|
|
|
|
|
|
|
|
110
|
5563
|
|
|
5563
|
1
|
23139
|
sub rng_new (@args) {
|
|
5563
|
|
|
|
|
11561
|
|
|
5563
|
|
|
|
|
7364
|
|
111
|
5563
|
|
|
|
|
7715
|
my $s;
|
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
# treat undef as an empty range
|
114
|
5563
|
|
100
|
|
|
11073
|
my @ranges = map { $_ // q() } @args;
|
|
15294
|
|
|
|
|
33431
|
|
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
try {
|
117
|
5563
|
|
|
5563
|
|
240303
|
$s = Set::IntSpan::Fast->new( @ranges );
|
118
|
|
|
|
|
|
|
} catch {
|
119
|
0
|
|
|
0
|
|
0
|
croak 'Common::rng_new ' . $_;
|
120
|
5563
|
|
|
|
|
30983
|
};
|
121
|
5563
|
|
|
|
|
1070912
|
return $s;
|
122
|
|
|
|
|
|
|
}
|
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
=head2 rng_valid($range)
|
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
Returns true if the range string is valid for Set::IntSpan::Fast. Valid
|
127
|
|
|
|
|
|
|
ranges consist of numbers, a pair of numbers delimited by dash
|
128
|
|
|
|
|
|
|
(e.g 15-75), or a mix of those delimited by commas (e.g. '5-9,12,25-30').
|
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
=cut
|
131
|
|
|
|
|
|
|
|
132
|
37
|
|
|
37
|
1
|
862
|
sub rng_valid ($rng) {
|
|
37
|
|
|
|
|
75
|
|
|
37
|
|
|
|
|
59
|
|
133
|
37
|
|
|
|
|
760
|
return $rng =~ $RANGE_LIST_RE;
|
134
|
|
|
|
|
|
|
}
|
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
=head2 rng_within($range, $domain)
|
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
Returns true if the range string is lies within the domain range. For
|
140
|
|
|
|
|
|
|
example rng_within('3-5', '1-12') return true, whereas
|
141
|
|
|
|
|
|
|
rng_within('1800,1950', '1900-2100') returns false because 1800 is
|
142
|
|
|
|
|
|
|
not within the domain of 1900 to 2100.
|
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
=cut
|
145
|
|
|
|
|
|
|
|
146
|
39
|
|
|
39
|
1
|
10876
|
sub rng_within ($rng, $domain) {
|
|
39
|
|
|
|
|
69
|
|
|
39
|
|
|
|
|
67
|
|
|
39
|
|
|
|
|
59
|
|
147
|
39
|
100
|
|
|
|
267
|
croak "*E* invalid range argument: $rng"
|
148
|
|
|
|
|
|
|
unless $rng =~ $RANGE_LIST_RE;
|
149
|
38
|
100
|
|
|
|
208
|
croak "*E* invalid domain argument: $rng"
|
150
|
|
|
|
|
|
|
unless $domain =~ $RANGE_LIST_RE;
|
151
|
|
|
|
|
|
|
|
152
|
37
|
|
|
|
|
90
|
my $rng_obj = rng_new($rng);
|
153
|
37
|
|
|
|
|
90
|
my $domain_obj = rng_new($domain);
|
154
|
|
|
|
|
|
|
|
155
|
37
|
|
|
|
|
135
|
return $rng_obj->subset($domain_obj);
|
156
|
|
|
|
|
|
|
}
|
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
=head2 tsv($list_or_list_of_lists)
|
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
Takes a perl list and returns an equivalent tab-separated string.
|
162
|
|
|
|
|
|
|
Alternatively, takes a list of lists and returns a newline-separated
|
163
|
|
|
|
|
|
|
string of tab-separated values.
|
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
=cut
|
166
|
|
|
|
|
|
|
|
167
|
28
|
|
|
28
|
1
|
5949
|
sub tsv ($list_or_list_of_lists) {
|
|
28
|
|
|
|
|
63
|
|
|
28
|
|
|
|
|
44
|
|
168
|
28
|
100
|
|
|
|
85
|
return $EMPTY if not defined $list_or_list_of_lists;
|
169
|
27
|
100
|
|
|
|
81
|
return $EMPTY if not $list_or_list_of_lists->@*;
|
170
|
|
|
|
|
|
|
|
171
|
26
|
|
|
|
|
69
|
my $argref = ref $list_or_list_of_lists->[0];
|
172
|
|
|
|
|
|
|
|
173
|
26
|
|
|
|
|
62
|
my $result = $EMPTY;
|
174
|
|
|
|
|
|
|
|
175
|
26
|
100
|
|
|
|
91
|
if ($argref eq 'ARRAY') {
|
|
|
100
|
|
|
|
|
|
176
|
14
|
|
|
|
|
31
|
my @rows;
|
177
|
14
|
|
|
|
|
34
|
foreach my $row_aref ( $list_or_list_of_lists->@* ) {
|
178
|
152
|
|
|
|
|
553
|
push @rows, join $TAB, $row_aref->@*;
|
179
|
|
|
|
|
|
|
}
|
180
|
14
|
|
|
|
|
94
|
$result = join $NL, @rows;
|
181
|
|
|
|
|
|
|
} elsif ($argref eq $EMPTY) {
|
182
|
11
|
|
|
|
|
53
|
$result = join $NL, $list_or_list_of_lists->@*;
|
183
|
|
|
|
|
|
|
} else {
|
184
|
1
|
|
|
|
|
12
|
croak '*E* tsv() invalid argument: ' . $argref;
|
185
|
|
|
|
|
|
|
}
|
186
|
|
|
|
|
|
|
|
187
|
25
|
|
|
|
|
430
|
return $result;
|
188
|
|
|
|
|
|
|
}
|
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
=head2 iso_date_time(@now)
|
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
Takes the first 6 elements from a perl localtime array and formats
|
194
|
|
|
|
|
|
|
them into an ISO date string YYYY-MM-DD HH:MM:SS.
|
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=cut
|
197
|
|
|
|
|
|
|
|
198
|
5
|
|
|
5
|
1
|
9801
|
sub iso_date_time (@now) {
|
|
5
|
|
|
|
|
13
|
|
|
5
|
|
|
|
|
7
|
|
199
|
|
|
|
|
|
|
## no critic [ProhibitMagicNumbers]
|
200
|
|
|
|
|
|
|
|
201
|
5
|
100
|
|
|
|
31
|
croak 'iso_date_time requires at least a 6-element localtime array'
|
202
|
|
|
|
|
|
|
if @now < 6;
|
203
|
|
|
|
|
|
|
|
204
|
3
|
|
|
|
|
12
|
my @ymdhms = ( $now[5]+1900, $now[4]+1, $now[3], $now[2], $now[1], $now[0] );
|
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
return wantarray
|
207
|
|
|
|
|
|
|
? @ymdhms
|
208
|
3
|
100
|
|
|
|
28
|
: sprintf '%4d-%02d-%02d %02d:%02d:%02d', @ymdhms
|
209
|
|
|
|
|
|
|
;
|
210
|
|
|
|
|
|
|
}
|
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
1;
|
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=head1 AUTHOR
|
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
Gary Puckering (jgpuckering@rogers.com)
|
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT
|
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
Copyright 2022, Gary Puckering
|
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
=cut
|