File Coverage

lib/DR/DateTime.pm
Criterion Covered Total %
statement 220 242 90.9
branch 62 78 79.4
condition 24 36 66.6
subroutine 44 51 86.2
pod 20 27 74.0
total 370 434 85.2


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