File Coverage

blib/lib/EBook/Ishmael/Time.pm
Criterion Covered Total %
statement 38 39 97.4
branch 7 8 87.5
condition n/a
subroutine 8 8 100.0
pod 3 3 100.0
total 56 58 96.5


line stmt bran cond sub pod time code
1             package EBook::Ishmael::Time;
2 18     18   162439 use 5.016;
  18         69  
3             our $VERSION = '2.03';
4 18     18   137 use strict;
  18         38  
  18         603  
5 18     18   117 use warnings;
  18         33  
  18         1091  
6              
7 18     18   130 use Exporter 'import';
  18         79  
  18         1236  
8             our @EXPORT = qw(guess_time format_locale_time format_rfc3339_time);
9              
10 18     18   8733 use Time::Piece;
  18         148274  
  18         123  
11              
12             my $WEEKDAY_RX = qr/(?
13             (Sun | Mon | Tue | Wed | Thu | Fri | Sat) |
14             (Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday)
15             )/x;
16             my $MONTH_NAME_RX = qr/(?
17             (
18             Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec
19             ) |
20             (
21             January | Febuary | March | April | May | June | July | August |
22             September | October | November | December
23             )
24             )/x;
25             my $CENTURY_NUM_RX = qr/(?[0-9]?[0-9])/;
26             my $MONTH_DAY_RX = qr/(?0?[1-9]|[12][0-9]|3[01])/;
27             my $HOUR_24_RX = qr/(?[01]?[0-9]|2[0-3])/;
28             my $HOUR_12_RX = qr/(?0?[1-9]|1[0-2])/;
29             my $YEAR_DAY_RX = qr/(?0?(0?[1-9]|[1-9]{2})|[1-2][1-9]{2}|3[0-6]{2})/;
30             my $MONTH_NUM_RX = qr/(?0?[1-9]|1[0-2])/;
31             my $MINUTE_RX = qr/(?[0-5]?[0-9])/;
32             my $AM_PM_RX = qr/(?

AM|PM>)/i;

33             my $SECONDS_RX = qr/(?[0-5]?[0-9]|6[01])/;
34             my $WEEK_NUM_RX = qr/(?[0-4]?[0-9]|5[0-3])/;
35             my $ORD_WEEK_DAY_RX = qr/(?[0-6])/;
36             my $CENTURY_YEAR_RX = qr/(?[0-9]?[0-9])/;
37             my $YEAR_RX = qr/(?[0-9]{1,4})/;
38             my $TZ_SPEC_RX = qr/(?Z|[+\-][0-9]{2}:?[0-9]{2})/;
39             my $TZ_NAME_RX = qr/(?[A-Z]{3})/;
40             my $EPOCH_RX = qr/(?-?[0-9]+)/;
41             my $HHMMSS_RX = qr/$HOUR_24_RX:$MINUTE_RX:$SECONDS_RX/;
42              
43             my @DATE_RXS = (
44             # strftime '%c' + '%Z'
45             qr/$WEEKDAY_RX\s+$MONTH_NAME_RX\s+$MONTH_DAY_RX\s+$HHMMSS_RX\s+$YEAR_RX\s+$TZ_NAME_RX/,
46             # strftime '%c' on my system (en_US.UTF-8)
47             qr/$WEEKDAY_RX\s+$MONTH_NAME_RX\s+$MONTH_DAY_RX\s+$HHMMSS_RX\s+$YEAR_RX/,
48             # RFC3339
49             qr/$YEAR_RX-$MONTH_NUM_RX-$MONTH_DAY_RX\x54$HHMMSS_RX$TZ_SPEC_RX/,
50             # RFC822
51             qr/$MONTH_DAY_RX\s+$MONTH_NAME_RX\s+$CENTURY_YEAR_RX\s+$HOUR_24_RX:$MINUTE_RX\s+$TZ_NAME_RX/,
52             qr/$MONTH_DAY_RX\s+$MONTH_NAME_RX\s+$CENTURY_YEAR_RX\s+$HOUR_24_RX:$MINUTE_RX\s+$TZ_SPEC_RX/,
53             # RFC1123
54             qr/$WEEKDAY_RX,\s+$MONTH_DAY_RX\s+$MONTH_NAME_RX\s+$YEAR_RX\s+$HHMMSS_RX\s+$TZ_NAME_RX/,
55             qr/$WEEKDAY_RX,\s+$MONTH_DAY_RX\s+$MONTH_NAME_RX\s+$YEAR_RX\s+$HHMMSS_RX\s+$TZ_SPEC_RX/,
56             # RFC850
57             qr/$WEEKDAY_RX,\s+$MONTH_DAY_RX-$MONTH_NAME_RX-$CENTURY_YEAR_RX\s+$HHMMSS_RX\s+$TZ_NAME_RX/,
58             # output of my date(1)
59             qr/$WEEKDAY_RX\s+$MONTH_NAME_RX\s+$MONTH_DAY_RX\s+$HHMMSS_RX\s+$AM_PM_RX\s+$TZ_NAME_RX\s+$YEAR_RX/,
60             # pdfinfo time format
61             qr/$WEEKDAY_RX\s+$MONTH_NAME_RX\s+$MONTH_DAY_RX\s+$HHMMSS_RX\s+$YEAR_RX\s+$TZ_NAME_RX/,
62             # Ruby date
63             qr/$WEEKDAY_RX\s+$MONTH_NAME_RX\s+$MONTH_DAY_RX\s+$HHMMSS_RX\s+$TZ_SPEC_RX\s+$YEAR_RX/,
64             # misc. date formats
65             qr/$MONTH_DAY_RX\.$MONTH_NUM_RX\.$CENTURY_YEAR_RX/,
66             qr/$MONTH_NUM_RX\.$MONTH_DAY_RX\.$CENTURY_YEAR_RX/,
67             qr/$MONTH_DAY_RX\/$MONTH_NUM_RX\/$CENTURY_YEAR_RX/,
68             qr/$MONTH_NUM_RX\/$MONTH_DAY_RX\/$CENTURY_YEAR_RX/,
69             qr/$MONTH_DAY_RX\.$MONTH_NUM_RX\.$YEAR_RX/,
70             qr/$MONTH_NUM_RX\.$MONTH_DAY_RX\.$YEAR_RX/,
71             qr/$MONTH_DAY_RX\/$MONTH_NUM_RX\/$YEAR_RX/,
72             qr/$MONTH_NUM_RX\/$MONTH_DAY_RX\/$YEAR_RX/,
73             qr/$YEAR_RX-$MONTH_NUM_RX-$MONTH_DAY_RX/,
74             qr/$YEAR_RX/,
75             qr/$EPOCH_RX/,
76             );
77              
78             my @FULL_MATCH_DATE_RXS = map { qr/^\s*$_\s*$/ } @DATE_RXS;
79              
80             my %POST_PROCS = (
81             # Some versions of Time::Piece can't handle colons in time zone specs
82             'z' => sub { $_[0] eq 'Z' ? '+0000' : $_[0] =~ s/://r },
83             );
84              
85             sub guess_time {
86              
87 50     50 1 249572 my ($str) = @_;
88              
89 50         95 my %matches;
90 50         87 my $found_match = 0;
91 50         132 for my $rx (@FULL_MATCH_DATE_RXS) {
92 384 100       2401 if ($str =~ $rx) {
93 50         101 $found_match = 1;
94 50         1776 %matches = %+;
95 50         284 last;
96             }
97             }
98 50 50       149 if (!$found_match) {
99 0         0 die "'$str' does not match any recognized date layout\n";
100             }
101              
102 50         137 my @codes;
103             my @parts;
104 50         172 for my $k (keys %matches) {
105 284         528 push @codes, "%$k";
106 284 100       592 if (exists $POST_PROCS{ $k }) {
107 33         135 push @parts, $POST_PROCS{ $k }->($matches{ $k });
108             } else {
109 251         536 push @parts, $matches{ $k };
110             }
111             }
112              
113 50         140 my $tp = eval { Time::Piece->strptime(join(' ', @parts), join(' ', @codes)) };
  50         680  
114 50 100       2414 if ($@ ne '') {
115 30         367 die "Failed to parse '$str'\n";
116             }
117 20         64 return $tp->epoch;
118              
119             }
120              
121             sub format_locale_time {
122              
123 12     12 1 30 my $time = shift;
124              
125 12         57 return gmtime($time)->strftime("%c");
126              
127             }
128              
129             sub format_rfc3339_time {
130              
131 34     34 1 13026 my $time = shift;
132              
133 34         151 return gmtime($time)->strftime("%Y-%m-%dT%H:%M:%S%z");
134              
135             }
136              
137             1;
138              
139             =head1 NAME
140              
141             EBook::Ishmael::Time - Time-handling subroutines for ishmael
142              
143             =head1 SYNOPSIS
144              
145             use EBook::Ishmael::Time qw(guess_time);
146              
147             my $t = guess_time("01.14.2025");
148              
149             =head1 DESCRIPTION
150              
151             B is a module that provides various time-handling
152             subroutines for L. This is a private module, please consult the
153             L manual for user documentation.
154              
155             =head1 SUBROUTINES
156              
157             =over 4
158              
159             =item $epoch = guess_time($str)
160              
161             C guesses the timestamp format of C<$str> and returns the number
162             seconds since the Unix epoch, or C if it could not identify the
163             timestamp format.
164              
165             =item $locale = format_locale_time($epoch)
166              
167             Formats the given time in the preferred format of the current locale.
168              
169             =item $rfc3339 = format_rfc3339_time($epoch)
170              
171             Formats the given time as an RFC3339 timestamp.
172              
173             =back
174              
175             =head1 AUTHOR
176              
177             Written by Samuel Young, Esamyoung12788@gmail.comE.
178              
179             This project's source can be found on its
180             L. Comments and pull
181             requests are welcome!
182              
183             =head1 COPYRIGHT
184              
185             Copyright (C) 2025-2026 Samuel Young
186              
187             This program is free software: you can redistribute it and/or modify
188             it under the terms of the GNU General Public License as published by
189             the Free Software Foundation, either version 3 of the License, or
190             (at your option) any later version.
191              
192             =cut