File Coverage

lib/DR/DateTime.pm
Criterion Covered Total %
statement 210 229 91.7
branch 56 70 80.0
condition 24 36 66.6
subroutine 42 48 87.5
pod 20 26 76.9
total 352 409 86.0


line stmt bran cond sub pod time code
1             package DR::DateTime;
2 7     7   424819 use DR::DateTime::Defaults;
  7         14  
  7         188  
3              
4 7     7   134 use 5.010001;
  7         17  
5 7     7   29 use strict;
  7         10  
  7         113  
6 7     7   26 use warnings;
  7         11  
  7         211  
7             our $VERSION = '0.07';
8 7     7   65 use Carp;
  7         13  
  7         271  
9              
10 7     7   30 use POSIX ();
  7         8  
  7         81  
11 7     7   1913 use Time::Local ();
  7         11650  
  7         161  
12 7     7   1888 use Time::Zone ();
  7         9382  
  7         187  
13 7     7   44 use feature 'state';
  7         12  
  7         2202  
14              
15             use overload
16 9     9   721 'bool' => sub { 1 },
17 3     3   187 '""' => sub { shift->strftime('%F %T%z') },
18             '<=>' => sub {
19 2     2   635 my ($self, $cv, $flip) = @_;
20 2 100       7 if ('DR::DateTime' eq ref $cv) {
21 1         4 return $self->fepoch <=> $cv->fepoch;
22             }
23 1         3 return $self->fepoch <=> $cv;
24             },
25              
26             'cmp' => sub {
27 2     2   647 my ($self, $cv, $flip) = @_;
28 2 100       8 if ('DR::DateTime' eq ref $cv) {
29 1         6 return $self->strftime('%F %T%z') cmp $cv->strftime('%F %T%z');
30             }
31 1         4 my $pct = $self->parse($cv);
32 1 50       3 return $self->strftime('%F %T%z') cmp $cv unless $pct;
33 1         6 return $self->strftime('%F %T%z') cmp $pct->strftime('%F %T%z');
34             },
35              
36 2     2   5 int => sub { shift->epoch }
37 7     7   5960 ;
  7         5590  
  7         82  
38              
39              
40             sub new {
41 45     45 1 3506 my ($class, $stamp, $tz) = @_;
42 45   66     103 $stamp //= time;
43              
44 45 100       86 if (defined $tz) {
45 39         135 $tz =~ /^([+-])?(\d{2})(\d{2})?$/;
46 39 50       100 croak "Wrong timezone format" unless defined $2;
47              
48 39   100     175 $tz = join '',
      100        
49             $1 // '+',
50             $2,
51             $3 // '00';
52             }
53              
54 45 100       86 $tz = $DR::DateTime::Defaults::TZFORCE
55             if defined $DR::DateTime::Defaults::TZFORCE;
56              
57 45   66     280 bless [ $stamp, $tz // () ] => ref($class) || $class;
      66        
58             }
59              
60             sub parse {
61 35     35 1 7980 my ($class, $str, $default_tz, $nocheck) = @_;
62 35 50       64 return undef unless defined $str;
63 35         53 my ($y, $m, $d, $H, $M, $S, $ns, $z);
64              
65 35         47 for ($str) {
66              
67 35 100       134 if (/^\d+$/) {
68 1   50     7 return $class->new($str, $default_tz // '+0000');
69             }
70 34 100       150 if (/^(\d{4})-(\d{2})-(\d{2})(?:\s+|T)(\d{2}):(\d{2}):(\d{2})(\.\d+)?\s*(\S+)?$/) {
71 28         114 ($y, $m, $d, $H, $M, $S, $ns, $z) =
72             ($1, $2, $3, $4, $5, $6, $7, $8);
73 28         191 goto PARSED;
74             }
75            
76 6 100       20 if (/^(\d{4})-(\d{2})-(\d{2})(?:\s+|T)(\d{2}):(\d{2})$/) {
77 2         9 ($y, $m, $d, $H, $M, $S, $ns, $z) =
78             ($1, $2, $3, $4, $5, 0, 0, undef);
79 2         16 goto PARSED;
80             }
81            
82 4 100       10 if (/^(\d{4})-(\d{2})-(\d{2})$/) {
83 1         4 ($y, $m, $d, $H, $M, $S, $ns, $z) =
84             ($1, $2, $3, 0, 0, 0, 0, undef);
85 1         7 goto PARSED;
86             }
87              
88 3 50       24 if (/^(\d{1,2})\.(\d{1,2})\.(\d{4})\s+(\d{2}):(\d{2}):(\d{2})(\.\d+)?\s*(\S+)?$/) {
89 3         14 ($y, $m, $d, $H, $M, $S, $ns, $z) =
90             ($3, $2, $1, $4, $5, $6, $7, $8);
91 3         19 goto PARSED;
92             }
93              
94 0         0 return undef;
95             }
96              
97              
98             PARSED:
99              
100 34   50     83 $z //= $default_tz // '+0000';
      66        
101 34         54 for ($z) {
102 34 50       91 if (/^[+-]\d{1,4}$/) {
103 34         84 s/^([+-])(\d|\d{3})$/${1}0$2/;
104 34         77 s/^([+-])(\d{2})$/${1}${2}00/;
105             } else {
106 0         0 croak "Wrong time zone format: '$z'";
107             }
108             }
109 34         41 for ($m) {
110 34         99 s/^0//;
111 34         84 $_--;
112             }
113 34         46 for ($d, $H, $M, $S) {
114 136         165 s/^0//;
115             }
116 34         51 $y -= 1900;
117              
118 34   100     87 $ns //= 0;
119 34         41 my $stamp = eval {
120 34     0   178 local $SIG{__DIE__} = sub {}; # Ick!
121 34 100       84 return Time::Local::timegm_nocheck($S,$M,$H,$d,$m,$y) if $nocheck;
122 25         66 Time::Local::timegm($S,$M,$H,$d,$m,$y);
123             };
124 34         969 $stamp += $ns;
125              
126 34         66 my $offset = Time::Zone::tz_offset($z, $stamp);
127 34         996 $class->new($stamp - $offset, $z);
128             }
129              
130 136     136 1 265 sub fepoch { shift->[0] }
131 281     281 1 5096 sub epoch { POSIX::floor(shift->[0]) }
132 195   66 195 1 2519 sub tz { shift->[1] // $DR::DateTime::Defaults::TZ }
133              
134             sub strftime :method {
135 123     123 1 202 my ($self, $format) = @_;
136 123 50       218 croak 'Invalid format' unless $format;
137 123         203 my $offset = Time::Zone::tz_offset($self->tz, $self->epoch);
138 123         3531 my $stamp = $self->epoch + $offset;
139 123         205 my $fstamp = $self->fepoch + $offset;
140              
141 123         129 state $patterns;
142 123 100       191 unless ($patterns) {
143             $patterns = {
144 0     0   0 '%' => sub { '%' },
145 47     47   78 'z' => sub { shift->tz },
146 0     0   0 'Z' => sub { shift->tz },
147             'N' => sub {
148 20     20   72 int(1_000_000_000 * abs($_[2] - $_[1])) }
149              
150 7         114 };
151 7         59 for my $sp (split //, 'aAbBcCdDeEFgGhHIjklmMnOpPrRsStTuUVwWxXyY') {
152 172     172   2842 $patterns->{$sp} = sub { POSIX::strftime "%$sp", gmtime $_[1] }
153 280         841 }
154             }
155              
156 123 50       463 $format =~ s{%([a-zA-Z%])}
  239         707  
157             { $patterns->{$1} ? $patterns->{$1}->($self, $stamp, $fstamp) : "%$1" }sgex;
158 123         593  
159             $format;
160             }
161              
162 7     7 1 12  
163             sub year { shift->strftime('%Y') }
164              
165 9     9 1 19 sub month {
166 9         27 for my $m (shift->strftime('%m')) {
167 9         38 $m =~ s/^0//;
168             return $m;
169             }
170             }
171              
172 28     28 1 45 sub day {
173 28         44 for my $d (shift->strftime('%d')) {
174 28         85 $d =~ s/^0//;
175             return $d;
176             }
177             }
178 2     2 1 6  
179             sub day_of_week { shift->strftime('%u') }
180 2     2 0 7  
181             sub quarter { POSIX::ceil(shift->month / 3) }
182              
183 11     11 1 1672 sub hour {
184 11         25 for my $h (shift->strftime('%H')) {
185 11         52 $h =~ s/^0//;
186             return $h;
187             }
188             }
189              
190 6     6 1 17 sub minute {
191 6         15 for my $m (shift->strftime('%M')) {
192 6         34 $m =~ s/^0//;
193             return $m;
194             }
195             }
196 4     4 1 15 sub second {
197 4         13 for my $s (shift->strftime('%S')) {
198 4         16 $s =~ s/^0//;
199             return $s;
200             }
201             }
202 4     4 1 448  
203             sub nanosecond { shift->strftime('%N') }
204              
205 2     2 0 7 sub hms {
206 2   50     7 my ($self, $sep) = @_;
207 2         5 $sep //= ':';
208 2         7 for ($sep) {
209             s/%/%%/g;
210 2         9 }
211             $self->strftime("%H$sep%M$sep%S");
212             }
213              
214 0     0 0 0 sub datetime {
215 0         0 my ($self) = @_;
216             return join 'T', $self->ymd, $self->hms;
217             }
218              
219 2     2 0 6 sub ymd {
220 2   50     7 my ($self, $sep) = @_;
221 2         5 $sep //= ':';
222 2         6 for ($sep) {
223             s/%/%%/g;
224 2         10 }
225             $self->strftime("%Y$sep%m$sep%d");
226             }
227 2     2 1 9  
228 0     0 1 0 sub time_zone { goto \&tz }
229             sub hires_epoch { goto \&fepoch }
230 9     9   13 sub _fix_date_after_arith_month {
231 9 100       16 my ($self, $new) = @_;
232 2 50       5 return $new->fepoch if $self->day == $new->day;
233 2         3 if ($new->day < $self->day) {
234             $new->[0] -= 86400;
235 2         6 }
236             $new->fepoch;
237             }
238 6     6 1 574 sub add {
239             my ($self, %set) = @_;
240 6         12
241 6 50       33 for my $n (delete $set{nanosecond}) {
242 0         0 last unless defined $n;
243             $self->[0] += $n / 1_000_000_000;
244             }
245 6         9  
246 6 100       12 for my $s (delete $set{second}) {
247 1         3 last unless defined $s;
248             $self->[0] += $s;
249             }
250 6         8  
251 6 50       10 for my $m (delete $set{minute}) {
252 0         0 last unless defined $m;
253             $self->[0] += $m * 60;
254             }
255 6         7
256 6 100       8 for my $h (delete $set{hour}) {
257 1         2 last unless defined $h;
258             $self->[0] += $h * 3600;
259             }
260 6         8  
261 6 100       7 for my $d (delete $set{day}) {
262 1         11 last unless defined $d;
263             $self->[0] += $d * 86400;
264             }
265 6         8  
266 6 100       11 for my $m (delete $set{month}) {
267 4         8 last unless defined $m;
268             my $nm = $self->month + $m;
269 4   100     16  
270 4         8 $set{year} //= 0;
271 1         2 while ($nm > 12) {
272 1         2 $nm -= 12;
273             $set{year}++;
274             }
275 4         7  
276 0         0 while ($nm < 1) {
277 0         0 $nm += 12;
278             $set{year}--;
279 4         8 }
280 4         16 my $str = $self->strftime('%F %T.%N %z');
  4         18  
281 4         9 $str =~ s/(\d{4})-\d{2}-/sprintf "%s-%02d-", $1, $nm/e;
282             $self->[0] =
283             $self->_fix_date_after_arith_month($self->parse($str, undef, 1));
284             }
285 6         11  
286 6 100       9 for my $y (delete $set{year}) {
287 5         9 last unless defined $y;
288 5         10 $y += $self->year;
289 5         17 my $str = $self->strftime('%F %T.%N %z');
290 5         8 $str =~ s/^\d{4}/$y/;
291             $self->[0] =
292             $self->_fix_date_after_arith_month($self->parse($str, undef, 1));
293 6         9 }
294             $self;
295             }
296              
297 0     0 0 0 sub subtract {
298             my ($self, %set) = @_;
299 0         0  
300 0         0 my %sub;
301 0         0 while (my ($k, $v) = each %set) {
302             $sub{$k} = -$v;
303 0         0 }
304             $self->add(%sub);
305             }
306              
307 6     6 1 15 sub truncate {
308             my ($self, %opts) = @_;
309 6   50     13  
310             my $to = $opts{to} // 'second';
311 6 100       11  
312 1         3 if ($to eq 'second') {
313 1         5 $self->[0] = $self->epoch;
314             return $self;
315             }
316 5         6  
317 5 100       9 my $str;
318 1         2 if ($to eq 'minute') {
319 1         5 $str = $self->strftime('%F %H:%M:00%z');
320             goto PARSE;
321             }
322 4 100       9  
323 1         3 if ($to eq 'hour') {
324 1         3 $str = $self->strftime('%F %H:00:00%z');
325             goto PARSE;
326             }
327 3 100       6
328 1         3 if ($to eq 'day') {
329 1         4 $str = $self->strftime('%F 00:00:00%z');
330             goto PARSE;
331             }
332 2 100       4  
333 1         2 if ($to eq 'month') {
334 1         3 $str = $self->strftime('%Y-%m-01 00:00:00%z');
335             goto PARSE;
336             }
337 1 50       3
338 1         2 if ($to eq 'year') {
339 1         3 $str = $self->strftime('%Y-01-01 00:00:00%z');
340             goto PARSE;
341             }
342 0         0  
343             croak "Can not truncate the datetime to '$to'";
344 5         10  
345             PARSE:
346 5         27 $self->[0] = $self->parse($str)->epoch;
347             $self;
348             }
349              
350 1     1 1 2 sub clone {
351 1   33     9 my ($self) = @_;
352             bless [ @$self ] => ref($self) || $self;
353             }
354              
355 1     1 1 4 sub set_time_zone {
356 1 50       6 my ($self, $tz) = @_;
357 1         4 if (defined $tz) {
358 1         17 for ($tz) {
359 1         10 s/^\d{1,4}$/+$&/;
360 1         4 s/^([+-])(\d)$/${1}0${2}00/;
361 1         5 s/^([+-]\d{2})$/${1}00/;
362             s/^([+-])(\d{3})$/${1}0$2/;
363             }
364 1 50       15  
    50          
365 0         0 if ($tz eq 'local') {
366             $tz = undef;
367 0         0 } elsif ($tz !~ /^[+-]\d{4}$/) {
368             croak "Wrong time zone: '$tz'";
369             }
370 1         3 }
371 1         5 $self->[1] = $tz;
372             $self;
373             }
374 1     1 0 11  
375             sub set_tz { goto \&set_time_zone }
376              
377             1;
378              
379             __END__