File Coverage

blib/lib/App/csvtool/Timetools.pm
Criterion Covered Total %
statement 125 126 99.2
branch 8 12 66.6
condition n/a
subroutine 27 27 100.0
pod 0 2 0.0
total 160 167 95.8


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2024 -- leonerd@leonerd.org.uk
5              
6             package App::csvtool::Timetools 0.04;
7              
8 13     13   17573 use v5.26;
  13         76  
9 13     13   75 use warnings;
  13         30  
  13         937  
10 13     13   74 use experimental 'signatures';
  13         155  
  13         129  
11              
12             =head1 NAME
13              
14             C - commands for F that handle timestamps
15              
16             =head1 DESCRIPTION
17              
18             This module provides commands for the F wrapper script that deal with
19             timestamp data in fields.
20              
21             =head2 Timestamp Parsing
22              
23             When parsing a timestamp in order to generate a UNIX epoch time, only the 6
24             basic fields (sec, min, hour, mday, mon, year) are used. Not all fields are
25             required.
26              
27             Any missing fields less significant than the ones provided by the format are
28             filled in with default zeroes (or 1 for the mday field). For example, a format
29             that specifies only the mday, mon and year fields will take a default time of
30             00:00:00 within each day.
31              
32             =cut
33              
34             =head1 COMMON OPTIONS
35              
36             Commands in this module recognise the following common options
37              
38             =head3 --timefmt
39              
40             Format string to use for formatting or parsing timestamps. Defaults to
41             ISO 8601 standard, i.e.
42              
43             %Y-%m-%dT%H:%M:%S
44              
45             =head3 --utc, -U
46              
47             Use UTC instead of local time.
48              
49             =cut
50              
51 13     13   10112 use POSIX qw( mktime strftime );
  13         116747  
  13         92  
52 13     13   39673 use Time::timegm qw( timegm );
  13         22284  
  13         1082  
53 13     13   7191 use POSIX::strptime qw( strptime );
  13         19980  
  13         1531  
54              
55             # Common opts
56 13         7770 use constant COMMON_COMMAND_OPTS => (
57             { name => "timefmt=", description => "Format string to parse timestamps",
58             default => "%Y-%m-%dT%H:%M:%S" },
59             { name => "utc|U", description => "Use UTC instead of local time" },
60 13     13   111 );
  13         31  
61              
62 3         6 sub formattime ( $pkg, $opts, $time )
  3         7  
63 3     3 0 9 {
  3         4  
  3         7  
64 3         7 my $TIMEFMT = $opts->{timefmt};
65 3 50       17 my @t = $opts->{utc} ? gmtime( $time ) : localtime( $time );
66              
67 3         127 return strftime( $TIMEFMT, @t );
68             }
69              
70 11         20 sub parsetime ( $pkg, $opts, $str )
  11         19  
71 11     11 0 20 {
  11         18  
  11         18  
72 11         22 my $TIMEFMT = $opts->{timefmt};
73 11         314 my @t = ( strptime $str, $TIMEFMT )[0..5]; # take only sec-year, ignore wday/yday
74 11 50       33 grep { defined } @t or
  66         154  
75             warn( "Unable to parse '$str' as a timestamp\n" ), return undef;
76              
77             # Fill in zeroes for undefined smaller fields
78 11         32 foreach my $i ( 0 .. 5 ) {
79 35 100       78 last if defined $t[$i];
80 24 50       60 $t[$i] = ( $i == 3 ) ? 1 : 0; # mday is 1-indexed
81             }
82              
83             # TODO: warn if any of [0]-[5] left undefined
84              
85 11 100       151 return $opts->{utc} ? timegm( @t[0..5] ) : mktime( @t[0..5] );
86             }
87              
88             =head1 COMMANDS
89              
90             =cut
91              
92             package App::csvtool::strftime
93             {
94 13     13   128 use base qw( App::csvtool::Timetools );
  13         29  
  13         1844  
95              
96             =head2 strftime
97              
98             $ csvtool strftime -fFIELD --timefmt=... FILE
99              
100             Formats a timestamp by using a C format, replacing the field with
101             the same time formatted as a string.
102              
103             =head3 --field, -f
104              
105             The field index to format the timestamp into (defaults to 1).
106              
107             =cut
108              
109 13     13   94 use constant COMMAND_DESC => "Format a timestamp from UNIX time";
  13         24  
  13         1309  
110              
111 13         1524 use constant COMMAND_OPTS => (
112             __PACKAGE__->COMMON_COMMAND_OPTS,
113             { name => "field|f=", description => "Field to use for timestamp",
114             default => 1 },
115 13     13   83 );
  13         35  
116              
117 13     13   145 use constant WANT_READER => 1;
  13         30  
  13         860  
118 13     13   98 use constant WANT_OUTPUT => 1;
  13         24  
  13         3637  
119              
120 1         3 sub run ( $pkg, $opts, $reader, $output )
  1         2  
  1         3  
121 1     1   19027 {
  1         2  
  1         2  
122 1         3 my $FIELD = $opts->{field}; $FIELD--;
  1         3  
123              
124 1         4 while( my $row = $reader->() ) {
125 3         1579 $row->[$FIELD] = __PACKAGE__->formattime( $opts, $row->[$FIELD] );
126              
127 3         12 $output->( $row );
128             }
129             }
130             }
131              
132             package App::csvtool::strptime
133             {
134 13     13   95 use base qw( App::csvtool::Timetools );
  13         29  
  13         1233  
135              
136             =head2 strptime
137              
138             $ csvtool strptime -fFIELD --timefmt=... FILE
139              
140             Parses a timestamp by using a C format, replacing the field with the
141             same time expressed as a UNIX epoch integer.
142              
143             =head3 --field, -f
144              
145             The field index to parse the timestamp from (defaults to 1).
146              
147             =cut
148              
149 13     13   85 use constant COMMAND_DESC => "Parse a timestamp into UNIX time";
  13         34  
  13         1372  
150              
151 13         926 use constant COMMAND_OPTS => (
152             __PACKAGE__->COMMON_COMMAND_OPTS,
153             { name => "field|f=", description => "Field to use for timestamp",
154             default => 1 },
155 13     13   85 );
  13         25  
156              
157 13     13   78 use constant WANT_READER => 1;
  13         26  
  13         753  
158 13     13   93 use constant WANT_OUTPUT => 1;
  13         30  
  13         3670  
159              
160 2         7 sub run ( $pkg, $opts, $reader, $output )
  2         3  
  2         4  
161 2     2   23864 {
  2         5  
  2         5  
162 2         5 my $FIELD = $opts->{field}; $FIELD--;
  2         6  
163              
164 2         7 while( my $row = $reader->() ) {
165 6         103 $row->[$FIELD] = __PACKAGE__->parsetime( $opts, $row->[$FIELD] );
166              
167 6         22 $output->( $row );
168             }
169             }
170             }
171              
172             package App::csvtool::tsort
173             {
174 13     13   101 use base qw( App::csvtool::Timetools );
  13         27  
  13         1202  
175              
176             =head2 tsort
177              
178             $ csvtool tsort -fFIELD --timefmt=... FILE
179              
180             A variant of the basic C command that parses a timestamp from a field
181             and sorts rows in chronological order based on those timestamps.
182              
183             =head3 --field, -f
184              
185             The field index to parse the sorting timestamp from (defaults to 1).
186              
187             =head3 --reverse, -r
188              
189             Reverses the order of sorting.
190              
191             =cut
192              
193 13     13   73 use constant COMMAND_DESC => "Sort rows into chronological order by a timestamp";
  13         24  
  13         1170  
194              
195 13         964 use constant COMMAND_OPTS => (
196             __PACKAGE__->COMMON_COMMAND_OPTS,
197             { name => "field|f=", description => "Field to use for timestamp",
198             default => 1 },
199             { name => "reverse|r", description => "Reverse order of sorting" },
200 13     13   78 );
  13         127  
201              
202 13     13   84 use constant WANT_READER => 1;
  13         22  
  13         896  
203 13     13   85 use constant WANT_OUTPUT => 1;
  13         24  
  13         6326  
204              
205 1         4 sub run ( $pkg, $opts, $reader, $output )
  1         4  
  1         2  
206 1     1   20827 {
  1         2  
  1         3  
207 1         3 my $FIELD = $opts->{field}; $FIELD--;
  1         4  
208              
209 1         2 my @rows;
210 1         4 while( my $row = $reader->() ) {
211             # Parse the timestamps on each line, rather than doing them all at
212             # once later using e.g. nsort_by {}, so that warnings come out at the
213             # right time
214 5         58 my $time = __PACKAGE__->parsetime( $opts, $row->[$FIELD] );
215 5         47 push @rows, [ $time, @$row ];
216             }
217              
218 1         11 @rows = sort { $a->[0] <=> $b->[0] } @rows;
  9         20  
219 1         6 shift @$_ for @rows; # remove timestamp keys
220              
221 1 50       5 if( $opts->{reverse} ) {
222 0         0 $output->( $_ ) for reverse @rows;
223             }
224             else {
225 1         6 $output->( $_ ) for @rows;
226             }
227             }
228             }
229              
230             =head1 AUTHOR
231              
232             Paul Evans
233              
234             =cut
235              
236             0x55AA;