File Coverage

lib/Weather/GHCN/Common.pm
Criterion Covered Total %
statement 76 77 98.7
branch 16 16 100.0
condition 4 4 100.0
subroutine 16 17 94.1
pod 6 6 100.0
total 118 120 98.3


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.011
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 8     8   115769 use v5.18; # minimum for Object::Pad
  8         33  
36            
37             package Weather::GHCN::Common;
38            
39             our $VERSION = 'v0.0.011';
40            
41            
42 8     8   51 use feature 'signatures';
  8         20  
  8         921  
43 8     8   54 no warnings 'experimental::signatures';
  8         26  
  8         303  
44            
45 8     8   43 use Exporter;
  8         20  
  8         334  
46 8     8   1835 use parent 'Exporter';
  8         1259  
  8         51  
47            
48 8     8   587 use Carp qw(croak);
  8         16  
  8         433  
49 8     8   469 use Const::Fast;
  8         2717  
  8         67  
50 8     8   2996 use Try::Tiny;
  8         10515  
  8         485  
51 8     8   3959 use Set::IntSpan::Fast;
  8         49453  
  8         7307  
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 43     43 1 1782 sub commify ($arg) {
  43         70  
  43         62  
87            
88 43   100     99 $arg //= q();
89            
90 43         104 my $text = reverse $arg;
91            
92 43         260 $text =~ s{ (\d\d\d) (?=\d) (?! \d* [.] ) }{$1,}xmsg;
93            
94 43         345 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 5597     5597 1 24169 sub rng_new (@args) {
  5597         12309  
  5597         7709  
111 5597         7810 my $s;
112            
113             # treat undef as an empty range
114 5597   100     11172 my @ranges = map { $_ // q() } @args;
  15331         32998  
115            
116             try {
117 5597     5597   246183 $s = Set::IntSpan::Fast->new( @ranges );
118             } catch {
119 0     0   0 croak 'Common::rng_new ' . $_;
120 5597         29516 };
121 5597         1087172 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 879 sub rng_valid ($rng) {
  37         70  
  37         57  
133 37         739 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 10885 sub rng_within ($rng, $domain) {
  39         86  
  39         70  
  39         60  
147 39 100       291 croak "*E* invalid range argument: $rng"
148             unless $rng =~ $RANGE_LIST_RE;
149 38 100       217 croak "*E* invalid domain argument: $rng"
150             unless $domain =~ $RANGE_LIST_RE;
151            
152 37         90 my $rng_obj = rng_new($rng);
153 37         86 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 29     29 1 6435 sub tsv ($list_or_list_of_lists) {
  29         55  
  29         48  
168 29 100       85 return $EMPTY if not defined $list_or_list_of_lists;
169 28 100       80 return $EMPTY if not $list_or_list_of_lists->@*;
170            
171 27         65 my $argref = ref $list_or_list_of_lists->[0];
172            
173 27         53 my $result = $EMPTY;
174            
175 27 100       89 if ($argref eq 'ARRAY') {
    100          
176 14         29 my @rows;
177 14         36 foreach my $row_aref ( $list_or_list_of_lists->@* ) {
178 152         584 push @rows, join $TAB, $row_aref->@*;
179             }
180 14         95 $result = join $NL, @rows;
181             } elsif ($argref eq $EMPTY) {
182 12         80 $result = join $NL, $list_or_list_of_lists->@*;
183             } else {
184 1         17 croak '*E* tsv() invalid argument: ' . $argref;
185             }
186            
187 26         451 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 9290 sub iso_date_time (@now) {
  5         14  
  5         7  
199             ## no critic [ProhibitMagicNumbers]
200            
201 5 100       32 croak 'iso_date_time requires at least a 6-element localtime array'
202             if @now < 6;
203            
204 3         9 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       26 : 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