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.009
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   93038 use v5.18; # minimum for Object::Pad
  7         26  
36            
37             package Weather::GHCN::Common;
38            
39             our $VERSION = 'v0.0.009';
40            
41            
42 7     7   41 use feature 'signatures';
  7         15  
  7         638  
43 7     7   39 no warnings 'experimental::signatures';
  7         15  
  7         255  
44            
45 7     7   36 use Exporter;
  7         12  
  7         301  
46 7     7   1669 use parent 'Exporter';
  7         1171  
  7         42  
47            
48 7     7   435 use Carp qw(croak);
  7         12  
  7         318  
49 7     7   473 use Const::Fast;
  7         2667  
  7         44  
50 7     7   1886 use Try::Tiny;
  7         5876  
  7         351  
51 7     7   3187 use Set::IntSpan::Fast;
  7         39615  
  7         5865  
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 1512 sub commify ($arg) {
  37         49  
  37         39  
87            
88 37   100     79 $arg //= q();
89            
90 37         81 my $text = reverse $arg;
91            
92 37         171 $text =~ s{ (\d\d\d) (?=\d) (?! \d* [.] ) }{$1,}xmsg;
93            
94 37         137 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 18888 sub rng_new (@args) {
  5563         9575  
  5563         6299  
111 5563         6408 my $s;
112            
113             # treat undef as an empty range
114 5563   100     8701 my @ranges = map { $_ // q() } @args;
  15294         28019  
115            
116             try {
117 5563     5563   201670 $s = Set::IntSpan::Fast->new( @ranges );
118             } catch {
119 0     0   0 croak 'Common::rng_new ' . $_;
120 5563         24628 };
121 5563         925284 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 761 sub rng_valid ($rng) {
  37         68  
  37         54  
133 37         713 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 8847 sub rng_within ($rng, $domain) {
  39         73  
  39         57  
  39         56  
147 39 100       235 croak "*E* invalid range argument: $rng"
148             unless $rng =~ $RANGE_LIST_RE;
149 38 100       210 croak "*E* invalid domain argument: $rng"
150             unless $domain =~ $RANGE_LIST_RE;
151            
152 37         90 my $rng_obj = rng_new($rng);
153 37         88 my $domain_obj = rng_new($domain);
154            
155 37         122 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 5132 sub tsv ($list_or_list_of_lists) {
  28         50  
  28         39  
168 28 100       70 return $EMPTY if not defined $list_or_list_of_lists;
169 27 100       65 return $EMPTY if not $list_or_list_of_lists->@*;
170            
171 26         62 my $argref = ref $list_or_list_of_lists->[0];
172            
173 26         41 my $result = $EMPTY;
174            
175 26 100       83 if ($argref eq 'ARRAY') {
    100          
176 14         27 my @rows;
177 14         28 foreach my $row_aref ( $list_or_list_of_lists->@* ) {
178 152         475 push @rows, join $TAB, $row_aref->@*;
179             }
180 14         97 $result = join $NL, @rows;
181             } elsif ($argref eq $EMPTY) {
182 11         51 $result = join $NL, $list_or_list_of_lists->@*;
183             } else {
184 1         9 croak '*E* tsv() invalid argument: ' . $argref;
185             }
186            
187 25         410 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 7722 sub iso_date_time (@now) {
  5         8  
  5         8  
199             ## no critic [ProhibitMagicNumbers]
200            
201 5 100       26 croak 'iso_date_time requires at least a 6-element localtime array'
202             if @now < 6;
203            
204 3         8 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       18 : 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