File Coverage

lib/DR/DateTime.pm
Criterion Covered Total %
statement 221 242 91.3
branch 62 78 79.4
condition 24 36 66.6
subroutine 44 50 88.0
pod 20 26 76.9
total 371 432 85.8


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