File Coverage

blib/lib/PDL/DateTime.pm
Criterion Covered Total %
statement 338 460 73.4
branch 146 284 51.4
condition 11 57 19.3
subroutine 59 78 75.6
pod 40 44 90.9
total 594 923 64.3


line stmt bran cond sub pod time code
1             package PDL::DateTime;
2              
3 5     5   1091758 use strict;
  5         14  
  5         156  
4 5     5   28 use warnings;
  5         12  
  5         179  
5 5     5   1035 use parent 'PDL';
  5         323  
  5         33  
6              
7             our $VERSION = '0.002';
8              
9 5     5   1482 use Scalar::Util 'looks_like_number';
  5         10  
  5         318  
10 5     5   4274 use POSIX ();
  5         36758  
  5         131  
11 5     5   814 use PDL::Types;
  5         2622  
  5         841  
12 5     5   1174 use PDL::Primitive;
  5         152119  
  5         43  
13 5     5   2902 use PDL::Basic qw(sequence);
  5         3170  
  5         35  
14 5     5   386 use PDL::Math qw(floor);
  5         8  
  5         37  
15 5     5   377 use PDL::Core qw(longlong long double byte short indx);
  5         8  
  5         30  
16 5     5   4035 use Time::Moment;
  5         7884  
  5         146  
17 5     5   33 use Carp;
  5         9  
  5         685  
18              
19 5         73 use overload '>' => \&_num_compare_gt,
20             '<' => \&_num_compare_lt,
21             '>=' => \&_num_compare_ge,
22             '<=' => \&_num_compare_le,
23             '==' => \&_num_compare_eq,
24             '!=' => \&_num_compare_ne,
25 5     5   25 '""' => \&_stringify;
  5         11  
26              
27             my %INC_SECONDS = (
28             week => 60 * 60 * 24 * 7,
29             day => 60 * 60 * 24,
30             hour => 60 * 60,
31             minute => 60,
32             second => 1,
33             );
34              
35             sub initialize {
36 1653     1653 0 88688 my ($class, %args) = @_;
37 1653 100       3218 $class = ref $class ? ref $class : $class;
38 1653         5115 return bless { %args, PDL => PDL->null }, $class;
39             }
40              
41             sub new {
42 10     10 1 8194 my ($class, $data, %opts) = @_;
43              
44             # for 'PDL::DateTime' just make a copy
45 10 100       35 return $data->copy(%opts) if ref $data eq 'PDL::DateTime';
46              
47 9         31 my $self = $class->initialize(%opts);
48             # $data is expected to contain epoch timestamps in microseconds
49 9 100       134 if (ref $data eq 'ARRAY') {
    50          
50 1         4 $self->{PDL} = longlong($data);
51             }
52             elsif (ref $data eq 'PDL') {
53 8 50       31 if ($data->type == longlong) {
    0          
54 8         247 $self->{PDL} = $data->copy;
55             # NOTE:
56             # $x = sequence(longlong, 6) # type LL
57             # $u = long($x) # == clone/copy of $x (type L)
58             # $u = longlong($x) # == same data, same type as $x
59             # $w = PDL->new($x) # == clone/copy of $x (type LL)
60             }
61             elsif ($data->type == double) {
62 0         0 $self->{PDL} = longlong(floor($data + 0.5));
63 0         0 $self->{PDL} -= $self->{PDL} % 1000; #truncate to milliseconds
64             }
65             else {
66 0         0 $self->{PDL} = longlong($data);
67             }
68             }
69             else {
70 0 0       0 if (looks_like_number $data) {
    0          
71 0         0 $self->{PDL} = longlong($data);
72             }
73             elsif ($data) {
74 0         0 $self->{PDL} = longlong(_datetime_to_jumboepoch($data));
75             }
76             else {
77 0         0 croak "PDL::DateTime->new: invalid data";
78             }
79             }
80              
81 9         547 return $self;
82             }
83              
84             # Derived objects need to supply its own copy!
85             sub copy {
86 102     102 0 3275 my ($self, %opts) = @_;
87 102         254 my $new = $self->initialize(%opts);
88             # copy the PDL
89 102         1383 $new->{PDL} = $self->{PDL}->SUPER::copy;
90             # copy the other stuff
91             #$new->{someThingElse} = $self->{someThingElse};
92 102         3336 return $new;
93             }
94              
95             sub new_from_epoch {
96 3     3 1 48 my ($class, $ep, %opts) = @_;
97 3         7 my $self = $class->initialize(%opts);
98 3 100       39 $ep = double($ep) if ref $ep eq 'ARRAY';
99             # convert epoch timestamp in seconds to microseconds
100 3         57 $self->{PDL} = longlong(floor(double($ep) * 1_000 + 0.5)) * 1000;
101 3         540 return $self;
102             }
103              
104             sub new_from_ratadie {
105 3     3 1 59 my ($class, $rd, %opts) = @_;
106 3         8 my $self = $class->initialize(%opts);
107 3 100       39 $rd = double($rd) if ref $rd eq 'ARRAY';
108             # EPOCH = (RD - 719_163) * 86_400
109             # only milisecond precision => strip microseconds
110 3         56 $self->{PDL} = longlong(floor((double($rd) - 719_163) * 86_400_000 + 0.5)) * 1000;
111 3         454 return $self;
112             }
113              
114             sub new_from_serialdate {
115 3     3 1 59 my ($class, $sd, %opts) = @_;
116 3         8 my $self = $class->initialize(%opts);
117 3 100       38 $sd = double($sd) if ref $sd eq 'ARRAY';
118             # EPOCH = (SD - 719_163 - 366) * 86_400
119             # only milisecond precision => strip microseconds
120 3         53 $self->{PDL} = longlong(floor((double($sd) - 719_529) * 86_400_000 + 0.5)) * 1000;
121 3         420 return $self;
122             }
123              
124             sub new_from_juliandate {
125 3     3 1 59 my ($class, $jd, %opts) = @_;
126 3         10 my $self = $class->initialize(%opts);
127 3 100       37 $jd = double($jd) if ref $jd eq 'ARRAY';
128             # EPOCH = (JD - 2_440_587.5) * 86_400
129             # only milisecond precision => strip microseconds
130 3         52 $self->{PDL} = longlong(floor((double($jd) - 2_440_587.5) * 86_400_000 + 0.5)) * 1000;
131 3         451 return $self;
132             }
133              
134             sub new_from_datetime {
135 32     32 1 3387 my ($class, $array, %opts) = @_;
136 32         91 my $self = $class->initialize(%opts);
137 32         503 $self->{PDL} = longlong _datetime_to_jumboepoch($array);
138 32         1843 return $self;
139             }
140              
141             sub new_from_parts {
142 3     3 1 16 my ($class, $y, $m, $d, $H, $M, $S, $U, %opts) = @_;
143 3 50 33     26 croak "new_from_parts: args - y, m, d - are mandatory" unless defined $y && defined $m && defined $d;
      33        
144 3         10 my $self = $class->initialize(%opts);
145 3 100       39 $y = long($y) if ref $y eq 'ARRAY';
146 3 100       57 $d = long($d) if ref $d eq 'ARRAY';
147 3 100       58 $m = long($m) if ref $m eq 'ARRAY';
148 3 100       59 $H = long($H) if ref $H eq 'ARRAY';
149 3 100       54 $M = long($M) if ref $M eq 'ARRAY';
150 3 100       54 $S = long($S) if ref $S eq 'ARRAY';
151 3 100       58 $U = long($U) if ref $U eq 'ARRAY';
152 3         54 my $rdate = _ymd2ratadie($y->copy, $m->copy, $d->copy);
153 3         92 my $epoch = (floor($rdate) - 719163) * 86400;
154 3 100       66 $epoch += floor($H) * 3600 if defined $H;
155 3 100       78 $epoch += floor($M) * 60 if defined $M;
156 3 100       59 $epoch += floor($S) if defined $S;
157 3         36 $epoch = longlong($epoch) * 1_000_000;
158 3 100       232 $epoch += longlong(floor($U)) if defined $U;
159 3         130 $self->{PDL} = longlong($epoch);
160 3         142 return $self;
161             }
162              
163             sub new_from_ymd {
164 1     1 1 34 my ($class, $ymd) = @_;
165 1         39 my $y = floor(($ymd/10000) % 10000);
166 1         41 my $m = floor(($ymd/100) % 100);
167 1         32 my $d = floor($ymd % 100);
168 1         8 return $class->new_from_parts($y, $m, $d);
169             }
170              
171             sub new_sequence {
172 44     44 1 19544 my ($class, $start, $count, $unit, $step, %opts) = @_;
173 44 50 33     233 croak "new_sequence: args - count, unit - are mandatory" unless defined $count && defined $unit;
174 44 100       98 $step = 1 unless defined $step;
175 44         125 my $self = $class->initialize(%opts);
176 44 50       642 my $tm_start = $start eq 'now' ? Time::Moment->now_utc : _dt2tm($start);
177 44         133 my $microseconds = $tm_start->microsecond;
178 44 100       106 if ($unit eq 'year') {
179             # slow :(
180 5         17 my @epoch = ($tm_start->epoch);
181 5         1698 push @epoch, $tm_start->plus_years($_*$step)->epoch for (1..$count-1);
182 5         19 $self->{PDL} = longlong(\@epoch) * 1_000_000 + $microseconds;
183             }
184 44 50       540 if ($unit eq 'quarter') {
185             # slow :(
186 0         0 my @epoch = ($tm_start->epoch);
187 0         0 push @epoch, $tm_start->plus_months(3*$_*$step)->epoch for (1..$count-1);
188 0         0 $self->{PDL} = longlong(\@epoch) * 1_000_000 + $microseconds;
189             }
190 44 100       136 if ($unit eq 'month') {
    100          
191             # slow :(
192 12         38 my @epoch = ($tm_start->epoch);
193 12         2982 push @epoch, $tm_start->plus_months($_*$step)->epoch for (1..$count-1);
194 12         41 $self->{PDL} = longlong(\@epoch) * 1_000_000 + $microseconds;
195             }
196             elsif (my $inc = $INC_SECONDS{$unit}) { # week day hour minute second
197 27         96 my $epoch = $tm_start->epoch;
198 27         95 $self->{PDL} = (longlong(floor(sequence($count) * $step * $inc + 0.5)) + $epoch) * 1_000_000 + $microseconds;
199             }
200 44         7950 return $self;
201             }
202              
203             sub double_epoch {
204 7     7 1 5526 my $self = shift;
205             # EP = JUMBOEPOCH / 1_000_000;
206 7         86 my $epoch_milisec = ($self - ($self % 1000)) / 1000; # BEWARE: precision only in milliseconds!
207 7         300 return double($epoch_milisec) / 1_000;
208             }
209              
210             sub longlong_epoch {
211 2     2 1 3295 my $self = shift;
212             # EP = JUMBOEPOCH / 1_000_000;
213             # BEWARE: precision only in seconds!
214 2         17 my $epoch_sec = ($self - ($self % 1_000_000)) / 1_000_000;
215 2         85 return longlong($epoch_sec->{PDL});
216             }
217              
218             sub double_ratadie {
219 42     42 1 6569 my $self = shift;
220             # RD = EPOCH / 86_400 + 719_163;
221 42         264 my $epoch_milisec = ($self - ($self % 1000)) / 1000; # BEWARE: precision only in milliseconds!
222 42         1382 return double($epoch_milisec) / 86_400_000 + 719_163;
223             }
224              
225             sub double_serialdate {
226 7     7 1 6147 my $self = shift;
227             # SD = EPOCH / 86_400 + 719_163 + 366;
228 7         55 my $epoch_milisec = ($self - ($self % 1000)) / 1000; # BEWARE: precision only in milliseconds!
229 7         268 return double($epoch_milisec) / 86_400_000 + 719_529;
230             }
231              
232             sub double_juliandate {
233 6     6 1 5730 my $self = shift;
234             # JD = EPOCH / 86_400 + 2_440_587.5;
235 6         51 my $epoch_milisec = ($self - ($self % 1000)) / 1000; # BEWARE: precision only in milliseconds!
236 6         279 return double($epoch_milisec) / 86_400_000 + 2_440_587.5;
237             }
238              
239             sub dt_ymd {
240 1     1 1 15 my $self = shift;
241 1         4 my ($y, $m, $d) = _ratadie2ymd($self->double_ratadie);
242 1         13 return (short($y), byte($m), byte($d));
243             }
244              
245             sub dt_year {
246 1     1 1 1258 my $self = shift;
247 1         4 my ($y, undef, undef) = _ratadie2ymd($self->double_ratadie);
248 1         15 return short($y);
249             }
250              
251             sub dt_quarter {
252 1     1 1 2 my $self = shift;
253 1         5 my (undef, $m, undef) = _ratadie2ymd($self->double_ratadie);
254 1         16 return ((byte($m)-1) / 3) + 1;
255             }
256              
257             sub dt_month {
258 1     1 1 385 my $self = shift;
259 1         4 my (undef, $m, undef) = _ratadie2ymd($self->double_ratadie);
260 1         14 return byte($m);
261             }
262              
263             sub dt_day {
264 1     1 1 373 my $self = shift;
265 1         4 my (undef, undef, $d) = _ratadie2ymd($self->double_ratadie);
266 1         16 return byte($d);
267             }
268              
269             sub dt_hour {
270 1     1 1 404 my $self = shift;
271 1         8 return PDL->new(byte((($self - ($self % 3_600_000_000)) / 3_600_000_000) % 24));
272             }
273              
274             sub dt_minute {
275 1     1 1 386 my $self = shift;
276 1         8 return PDL->new(byte((($self - ($self % 60_000_000)) / 60_000_000) % 60));
277             }
278              
279             sub dt_second {
280 1     1 1 380 my $self = shift;
281 1         7 return PDL->new(byte((($self - ($self % 1_000_000)) / 1_000_000) % 60));
282             }
283              
284             sub dt_microsecond {
285 1     1 1 404 my $self = shift;
286 1         7 return PDL->new(long($self % 1_000_000));
287             }
288              
289             sub dt_day_of_week {
290 1     1 1 420 my $self = shift;
291 1         8 my $days = ($self - ($self % 86_400_000_000)) / 86_400_000_000;
292 1         39 return PDL->new(byte(($days + 3) % 7) + 1); # 1..Mon, 7..Sun
293             }
294              
295             sub dt_day_of_year {
296 1     1 1 444 my $self = shift;
297 1         7 my $rd1 = long(floor($self->double_ratadie));
298 1         95 my $rd2 = long(floor($self->dt_align('year')->double_ratadie));
299 1         100 return PDL->new(short, ($rd1 - $rd2 + 1));
300             }
301              
302             sub dt_add {
303 48     48 1 392 my $self = shift;
304 48 100       227 if ($self->is_inplace) {
305 16         59 $self->set_inplace(0);
306 16         44 while (@_) {
307 16         29 my ($unit, $num) = (shift, shift);
308 16 100       89 if ($unit eq 'month') {
    50          
    100          
    100          
    100          
    50          
309 3         9 $self += $self->_plus_delta_m($num);
310             }
311             elsif ($unit eq 'quarter') {
312 0         0 $self += $self->_plus_delta_m($num * 3);
313             }
314             elsif ($unit eq 'year') {
315 4         98 $self += $self->_plus_delta_m($num * 12);
316             }
317             elsif ($unit eq 'millisecond') {
318 1         5 $self += $num * 1000;
319             }
320             elsif ($unit eq 'microsecond') {
321 1         4 $self += $num;
322             }
323             elsif (my $inc = $INC_SECONDS{$unit}) { # week day hour minute second
324 7         80 my $add = longlong(floor($num * $inc * 1_000_000 + 0.5));
325 7         368 $self->inplace->plus($add, 0);
326             }
327             }
328 16         306 return $self;
329             }
330             else {
331 32         76 my $rv = $self->copy;
332 32         96 while (@_) {
333 32         63 my ($unit, $num) = (shift, shift);
334 32 100       165 if ($unit eq 'month') {
    50          
    100          
    100          
    100          
    50          
335 6         13 $rv += $rv->_plus_delta_m($num);
336             }
337             elsif ($unit eq 'quarter') {
338 0         0 $rv += $rv->_plus_delta_m($num * 3);
339             }
340             elsif ($unit eq 'year') {
341 8         20 $rv += $rv->_plus_delta_m($num * 12);
342             }
343             elsif ($unit eq 'millisecond') {
344 2         12 $rv += $num * 1000;
345             }
346             elsif ($unit eq 'microsecond') {
347 2         10 $rv += $num;
348             }
349             elsif(my $inc = $INC_SECONDS{$unit}) { # week day hour minute second
350 14         1673 $rv += longlong(floor($num * $inc * 1_000_000 + 0.5));
351             }
352             }
353 32         1493 return $rv;
354             }
355             }
356              
357             sub dt_align {
358 24     24 1 2395 my ($self, $unit, $up) = @_;
359 24 100       100 if ($self->is_inplace) {
360 8         20 $self->set_inplace(0);
361 8 50       21 return $self unless defined $unit;
362 8 100       47 if ($unit eq 'year') {
    50          
    100          
    100          
    50          
363 1         4 $self->{PDL} = $self->_allign_myq(0, 1, 0, $up)->{PDL};
364             }
365             elsif ($unit eq 'quarter') {
366 0         0 $self->{PDL} = $self->_allign_myq(0, 0, 1, $up)->{PDL};
367             }
368             elsif ($unit eq 'month') {
369 1         4 $self->{PDL} = $self->_allign_myq(1, 0, 0, $up)->{PDL};
370             }
371             elsif ($unit eq 'millisecond') {
372 1         7 my $sub = $self % 1_000;
373 1         26 $self->inplace->minus($sub, 0);
374             }
375             elsif (my $inc = $INC_SECONDS{$unit}) { # week day hour minute second
376 5 100       36 my $sub = $unit eq 'week' ? ($self + 3 * 60 * 60 * 24 * 1_000_000) % ($inc * 1_000_000) : $self % ($inc * 1_000_000);
377 5 50 33     127 $sub -= 6 * 60 * 60 * 24 * 1_000_000 if $up && $unit eq 'week';
378 5         18 $self->inplace->minus($sub, 0);
379             }
380 8         160 return $self;
381             }
382             else {
383 16 50       40 return unless defined $unit;
384 16 100       94 if ($unit eq 'year') {
    50          
    100          
    100          
    50          
385 3         13 return $self->_allign_myq(0, 1, 0, $up);
386             }
387             elsif ($unit eq 'quarter') {
388 0         0 return $self->_allign_myq(0, 0, 1, $up);
389             }
390             elsif ($unit eq 'month') {
391 2         9 return $self->_allign_myq(1, 0, 0, $up);
392             }
393             elsif ($unit eq 'millisecond') {
394 1         7 return $self - $self % 1_000;
395             }
396             elsif (my $inc = $INC_SECONDS{$unit}) { # week day hour minute second
397 10 100       81 my $sub = $unit eq 'week' ? ($self + 3 * 60 * 60 * 24 * 1_000_000) % ($inc * 1_000_000) : $self % ($inc * 1_000_000);
398 10 50 33     306 $sub -= 6 * 60 * 60 * 24 * 1_000_000 if $up && $unit eq 'week';
399 10         48 return $self - $sub;
400             }
401             }
402             }
403              
404             sub dt_at {
405 5     5 1 9 my $self = shift;
406 5 50       16 my $fmt = looks_like_number($_[-1]) ? 'auto' : pop;
407 5         22 my $v = PDL::Core::at_c($self, [@_]);
408 5 50 33     38 $fmt = $self->_autodetect_strftime_format if !$fmt || $fmt eq 'auto';
409 5         235 return _jumboepoch_to_datetime($v, $fmt);
410             }
411              
412             sub dt_set {
413 4     4 1 8 my $self = shift;
414 4         7 my $datetime = pop;
415 4         12 PDL::Core::set_c($self, [@_], _datetime_to_jumboepoch($datetime));
416             }
417              
418             sub dt_unpdl {
419 113     113 1 5999 my ($self, $fmt) = @_;
420 113 100 66     550 $fmt = $self->_autodetect_strftime_format if !$fmt || $fmt eq 'auto';
421 113 100       3132 if ($fmt eq 'epoch') {
    100          
422 1         6 return (double($self) / 1_000_000)->unpdl;
423             }
424             elsif ($fmt eq 'epoch_int') {
425 1         13 return longlong(($self - ($self % 1_000_000)) / 1_000_000)->unpdl;
426             }
427             else {
428 111         346 my $array = $self->unpdl;
429 111         2683 _jumboepoch_to_datetime($array, $fmt, 1); # change $array inplace!
430 111         638 return $array;
431             }
432             }
433              
434             sub dt_diff {
435 0     0 1 0 my ($self, $unit) = @_;
436 0 0       0 return PDL->new('BAD')->reshape(1) if $self->nelem == 1;
437 0         0 my $rv = PDL->new(longlong, 'BAD')->glue(0, $self->slice("1:-1") - $self->slice("0:-2"));
438 0 0       0 return $rv unless $unit;
439 0 0       0 return double($rv) / 604_800_000_000 if $unit eq 'week';
440 0 0       0 return double($rv) / 86_400_000_000 if $unit eq 'day';
441 0 0       0 return double($rv) / 3_600_000_000 if $unit eq 'hour';
442 0 0       0 return double($rv) / 60_000_000 if $unit eq 'minute';
443 0 0       0 return double($rv) / 1_000_000 if $unit eq 'second';
444 0 0       0 return double($rv) / 1_000 if $unit eq 'millisecond';
445 0         0 croak "dt_diff: invalid unit '$unit'";
446             }
447              
448             sub dt_periodicity {
449 0     0 1 0 my $self = shift;
450 0         0 my $freq = $self->dt_diff->median;
451 0 0       0 return '' if $freq eq 'BAD';
452 0 0 0     0 if ($freq < 1_000 ) {
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
453             # $freq < 1 millisecond
454 0         0 return "microsecond";
455             }
456             elsif ($freq < 1_000_000 ) {
457             # 1 millisecond <= $freq < 1 second
458 0         0 return "millisecond";
459             }
460             elsif ($freq < 60_000_000 ) {
461             # 1 second <= $freq < 1 minute
462 0         0 return "second";
463             }
464             elsif ($freq < 3_600_000_000) {
465             # 1 minute <= $freq < 1 hour
466 0         0 return "minute";
467             }
468             elsif ($freq < 86_400_000_000) {
469             # 1 hour <= $freq < 24 hours
470 0         0 return "hour";
471             }
472             elsif ($freq == 86_400_000_000) {
473             # 24 hours
474 0         0 return "day";
475             }
476             elsif ($freq == 604_800_000_000) {
477             # 7 days
478 0         0 return "week";
479             }
480             elsif ($freq >= 2_419_200_000_000 && $freq <= 2_678_400_000_000 ) {
481             # 28days <= $freq <= 31days
482 0         0 return "month";
483             }
484             elsif ($freq >= 7_776_000_000_000 && $freq <= 7_948_800_000_000 ) {
485             # 90days <= $freq <= 92days
486 0         0 return "quarter";
487             }
488             elsif ($freq >= 31_536_000_000_000 && $freq <= 31_622_400_000_000 ) {
489             # 365days <= $freq <= 366days
490 0         0 return "year";
491             }
492 0         0 return ''; # unknown
493             }
494              
495             sub dt_startpoints {
496 0     0 1 0 my ($self, $unit) = @_;
497 0 0       0 croak "dt_startpoints: undefined unit" unless $unit;
498 0 0       0 croak "dt_startpoints: 1D piddle required" unless $self->ndims == 1;
499 0 0       0 croak "dt_startpoints: input not increasing" unless $self->is_increasing;
500 0         0 return indx(0)->append($self->dt_endpoints($unit)->slice("0:-2") + 1);
501             }
502              
503             sub dt_endpoints {
504 0     0 1 0 my ($self, $unit) = @_;
505 0 0       0 croak "dt_endpoints: undefined unit" unless $unit;
506 0 0       0 croak "dt_endpoints: 1D piddle required" unless $self->ndims == 1;
507 0 0       0 croak "dt_endpoints: input not increasing" unless $self->is_increasing;
508 0         0 my $diff = $self->dt_align($unit)->dt_diff;
509 0         0 my $end = which($diff != 0) - 1;
510 0 0       0 if ($end->nelem == 0) {
511 0         0 $end = indx([$self->nelem-1]);
512             }
513             else {
514 0 0       0 $end = $end->append($self->nelem-1) unless $end->at($end->nelem-1) == $end->nelem-1;
515             }
516 0         0 return indx($end);
517             }
518              
519             sub dt_slices {
520 0     0 1 0 my ($self, $unit) = @_;
521 0 0       0 croak "dt_slices: undefined unit" unless $unit;
522 0 0       0 croak "dt_slices: 1D piddle required" unless $self->ndims == 1;
523 0 0       0 croak "dt_slices: input not increasing" unless $self->is_increasing;
524 0         0 my $end = $self->dt_endpoints($unit);
525 0         0 my $start = indx([0]);
526 0 0       0 $start = $start->append($end->slice("0:-2") + 1) if $end->nelem > 1;
527 0         0 return $start->cat($end)->transpose;
528             }
529              
530             sub dt_nperiods {
531 0     0 1 0 my ($self, $unit) = @_;
532 0 0       0 croak "dt_nperiods: undefined unit" unless $unit;
533 0         0 return $self->dt_endpoints($unit)->nelem;
534             }
535              
536             sub is_increasing {
537 0     0 1 0 my ($self, $strictly) = @_;
538 0 0       0 return !(which($self->dt_diff <= 0)->nelem > 0) if $strictly;
539 0         0 return !(which($self->dt_diff < 0)->nelem > 0);
540             }
541              
542             sub is_decreasing {
543 0     0 1 0 my ($self, $strictly) = @_;
544 0 0       0 return !(which($self->dt_diff >= 0)->nelem > 0) if $strictly;
545 0         0 return !(which($self->dt_diff > 0)->nelem > 0);
546             }
547              
548             sub is_uniq {
549 0     0 1 0 my $self = shift;
550 0         0 my $diff = $self->qsort->dt_diff;
551 0         0 return !(which($diff == 0)->nelem > 0);
552             }
553              
554             sub is_regular {
555 0     0 1 0 my $self = shift;
556 0         0 my $dt = $self->dt_diff;
557 0         0 my $diff = $self->dt_diff->qsort;
558 0         0 my $min = $diff->min;
559 0         0 my $max = $diff->max;
560 0   0     0 return ($min ne "BAD") && ($max ne "BAD") && ($min == $max) && ($max > 0);
561             }
562              
563             ### private methods
564              
565             sub _stringify {
566 1     1   285 my $self = shift;
567 1 50       9 my $data = $self->ndims > 0 ? $self->dt_unpdl : $self->dt_unpdl->[0];
568 1         4 my $rv = _print_array($data, 0);
569 1         5 $rv =~ s/\n$//;
570 1         11 return $rv;
571             }
572              
573             sub _num_compare_gt {
574 0     0   0 my ($self, $other, $swap) = @_;
575 0 0 0     0 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
576 0         0 PDL::gt($self, $other, $swap);
577             }
578              
579             sub _num_compare_lt {
580 0     0   0 my ($self, $other, $swap) = @_;
581 0 0 0     0 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
582 0         0 PDL::lt($self, $other, $swap);
583             }
584              
585             sub _num_compare_ge {
586 0     0   0 my ($self, $other, $swap) = @_;
587 0 0 0     0 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
588 0         0 PDL::ge($self, $other, $swap);
589             }
590              
591             sub _num_compare_le {
592 0     0   0 my ($self, $other, $swap) = @_;
593 0 0 0     0 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
594 0         0 PDL::le($self, $other, $swap);
595             }
596              
597             sub _num_compare_eq {
598 27     27   7641 my ($self, $other, $swap) = @_;
599 27 0 33     88 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
600 27         170 PDL::eq($self, $other, $swap);
601             }
602              
603             sub _num_compare_ne {
604 266     266   4818 my ($self, $other, $swap) = @_;
605 266 50 33     1094 $other = PDL::DateTime->new_from_datetime($other) if !ref $other && !looks_like_number($other);
606 266         1436 PDL::ne($self, $other, $swap);
607             }
608              
609             sub _autodetect_strftime_format {
610 75     75   122 my $self = shift;
611 75 100       496 if (which(($self % (24*60*60*1_000_000)) != 0)->nelem == 0) {
    100          
    100          
    100          
612 8         161 return "%Y-%m-%d";
613             }
614             elsif (which(($self % (60*1_000_000)) != 0)->nelem == 0) {
615 4         79 return "%Y-%m-%dT%H:%M";
616             }
617             elsif (which(($self % 1_000_000) != 0)->nelem == 0) {
618 2         42 return "%Y-%m-%dT%H:%M:%S";
619             }
620             elsif (which(($self % 1_000) != 0)->nelem == 0) {
621 2         40 return "%Y-%m-%dT%H:%M:%S.%3N";
622             }
623             else {
624 59         1267 return "%Y-%m-%dT%H:%M:%S.%6N";
625             }
626             }
627              
628             sub _plus_delta_m {
629 21     21   37 my ($self, $delta_m) = @_;
630 21         142 my $day_fraction = $self % 86_400_000_000;
631 21         606 my $rdate_bf = ($self - $day_fraction)->double_ratadie;
632 21         840 my ($y, $m, $d) = _ratadie2ymd($rdate_bf);
633 21         52 my $rdate_af = _ymd2ratadie($y, $m, $d, $delta_m);
634 21         171 my $rv = longlong($rdate_af - $rdate_bf) * 86_400_000_000;
635 21         1575 return $rv;
636             }
637              
638             sub _allign_myq {
639 7     7   12 my ($self, $mflag, $yflag, $qflag, $up) = @_;
640 7         18 my $rdate = $self->double_ratadie;
641 7         244 my ($y, $m, $d) = _ratadie2ymd($rdate);
642 7 50       33 $m .= $up ? 12 : 1 if $yflag;
    100          
643 7 0       118 $m = $up ? $m+((3-$m)%3) : $m-(($m-1)%3) if $qflag;
    50          
644 7 50       27 $d .= $up ? _days_in_month($y, $m) : 1;
645 7         126 $rdate = _ymd2ratadie($y, $m, $d);
646 7         128 return PDL::DateTime->new(longlong(floor($rdate) - 719163) * 86_400_000_000);
647             }
648              
649             ### public functions (used e.g. by PDL::IO::CSV)
650              
651             sub dt2ll {
652 0     0 0 0 eval {
653 0         0 my $tm = Time::Moment->from_string(_fix_datetime_value(shift), lenient=>1);
654 0         0 $tm->epoch * 1_000_000 + $tm->microsecond;
655             };
656             }
657              
658             sub ll2dt {
659 0     0 0 0 my $v = shift;
660 0         0 my $us = int($v % 1_000_000);
661 0         0 my $ts = int(($v - $us) / 1_000_000);
662 0 0       0 my $rv = eval { Time::Moment->from_epoch($ts, $us * 1000)->to_string(reduced=>1) } or return;
  0         0  
663 0         0 $rv =~ s/(T00:00)?Z$//;
664 0         0 return $rv;
665             }
666              
667             ### private functions
668              
669             sub _dt2tm {
670 648     648   801 eval { Time::Moment->from_string(_fix_datetime_value(shift), lenient=>1) };
  648         1132  
671             }
672              
673             sub _ll2tm {
674 768     768   975 my $v = shift;
675 768         1028 my $us = int($v % 1_000_000);
676 768         1147 my $ts = int(($v - $us) / 1_000_000);
677 768         956 eval { Time::Moment->from_epoch($ts, $us * 1000) };
  768         2885  
678             }
679              
680             sub _print_array {
681 1     1   3 my ($val, $level) = @_;
682 1         3 my $prefix = " " x $level;
683 1 50 33     9 if (ref $val eq 'ARRAY' && !ref $val->[0]) {
    0          
684 1         5 return $prefix . join(" ", '[', @$val, ']') . "\n";
685             }
686             elsif (ref $val eq 'ARRAY') {
687 0         0 my $out = $prefix."[\n";
688 0         0 $out .= _print_array($_, $level + 1) for (@$val);
689 0         0 $out .= $prefix."]\n";
690             }
691             else {
692 0         0 return $prefix . $val . "\n";
693             }
694             }
695              
696             sub _fix_datetime_value {
697 648     648   790 my $v = shift;
698             # '2015-12-29' > '2015-12-29T00Z'
699 648 100       1945 return $v."T00Z" if $v =~ /^\d\d\d\d-\d\d-\d\d$/;
700             # '2015-12-29 11:59' > '2015-12-29 11:59Z'
701 647 50       1674 return $v."Z" if $v =~ /^\d\d\d\d-\d\d-\d\d[ T]\d\d:\d\d$/;
702             # '2015-12-29 11:59:11' > '2015-12-29 11:59:11Z' or '2015-12-29 11:59:11.123' > '2015-12-29 11:59:11.123Z'
703 647 100       4537 return $v."Z" if $v =~ /^\d\d\d\d-\d\d-\d\d[ T]\d\d:\d\d:\d\d(\.\d+)?$/;
704 58         362 return $v;
705             }
706              
707             sub _datetime_to_jumboepoch {
708 636     636   913 my ($dt, $inplace) = @_;
709 636         675 my $tm;
710 636 100       1154 if (ref $dt eq 'ARRAY') {
711 32         37 my @new;
712 32         71 for (@$dt) {
713 600         1136 my $s = _datetime_to_jumboepoch($_, $inplace);
714 600 50       1048 if ($inplace) {
715 0 0       0 $_ = (ref $_ ? undef : $s) if ref $_ ne 'ARRAY';
    0          
716             }
717             else {
718 600         1143 push @new, $s;
719             }
720             }
721 32 50       173 return \@new if !$inplace;
722             }
723             else {
724 604 50 0     1859 if (looks_like_number $dt) {
    50          
    0          
    0          
725 0         0 return int POSIX::floor($dt * 1_000_000 + 0.5);
726             }
727             elsif (!ref $dt) {
728 604 50       1354 $tm = ($dt eq 'now') ? Time::Moment->now_utc : _dt2tm($dt);
729             }
730             elsif (ref $dt eq 'DateTime' || ref $dt eq 'Time::Piece') {
731 0         0 $tm = eval { Time::Moment->from_object($dt) };
  0         0  
732             }
733             elsif (ref $dt eq 'Time::Moment') {
734 0         0 $tm = $dt;
735             }
736 604 50       4940 return undef unless $tm;
737 604         2692 return int($tm->epoch * 1_000_000 + $tm->microsecond);
738             }
739             }
740              
741             sub _jumboepoch_to_datetime {
742 879     879   1397 my ($v, $fmt, $inplace) = @_;
743 879 50       1697 return 'BAD' unless defined $v;
744 879 100       2588 if (ref $v eq 'ARRAY') {
    50          
745 111         180 my @new;
746 111         240 for (@$v) {
747 763         1400 my $s = _jumboepoch_to_datetime($_, $fmt, $inplace);
748 763 50       1406 if ($inplace) {
749 763 50       2173 $_ = $s if ref $_ ne 'ARRAY';
750             }
751             else {
752 0         0 push @new, $s;
753             }
754             }
755 111 50       912 return \@new if !$inplace;
756             }
757             elsif (!ref $v) {
758 768         1341 my $tm = _ll2tm($v);
759 768 50       1639 return 'BAD' unless defined $tm;
760 768 50       1346 if ($fmt eq 'Time::Moment') {
761 0         0 return $tm;
762             }
763             else {
764 768         3771 return $tm->strftime($fmt);
765             }
766             }
767             }
768              
769             my $DAYS_PER_400_YEARS = 146_097;
770             my $DAYS_PER_100_YEARS = 36_524;
771             my $DAYS_PER_4_YEARS = 1_461;
772             my $MAR_1_TO_DEC_31 = 306;
773              
774             sub _ymd2ratadie {
775 31     31   273 my ($y, $m, $d, $delta_m) = @_;
776             # based on Rata Die calculation from https://metacpan.org/source/DROLSKY/DateTime-1.10/lib/DateTime.xs#L151
777             # RD: 1 => 0001-01-01
778             # RD: 2 => 0001-01-02
779             # RD: 719163 => 1970-01-01
780             # RD: 730120 => 2000-01-01
781             # RD: 2434498 => 6666-06-06
782             # RD: 3652059 => 9999-12-31
783              
784 31 100       77 if (defined $delta_m) {
785             # handle months + years
786 21         66 $m->inplace->plus($delta_m - 1, 0);
787 21         807 my $extra_y = floor($m / 12);
788 21         157 $m->inplace->modulo(12, 0);
789 21         536 $m->inplace->plus(1, 0);
790 21         498 $y->inplace->plus($extra_y, 0);
791             # fix days
792 21         1547 my $dec_by_one = ($d==31) * (($m==4) + ($m==6) + ($m==9) + ($m==11));
793             # 1800, 1900, 2100, 2200, 2300 - common; 2000, 2400 - leap
794 21         1547 my $is_nonleap_yr = (($y % 4)!=0) + (($y % 100)==0) - (($y % 400)==0);
795 21         1209 my $dec_nonleap_feb = ($m==2) * ($d>28) * $is_nonleap_yr * ($d-28);
796 21         1146 my $dec_leap_feb = ($m==2) * ($d>29) * (1 - $is_nonleap_yr) * ($d-29);
797 21         424 $d->inplace->minus($dec_by_one + $dec_leap_feb + $dec_nonleap_feb, 0);
798             }
799              
800 31         850 my $rdate = double($d); # may contain day fractions
801 31         2477 $rdate->setbadif(($y < 1) + ($y > 9999));
802 31         1212 $rdate->setbadif(($m < 1) + ($m > 12));
803 31         1217 $rdate->setbadif(($d < 1) + ($d >= 32)); # not 100% correct (max. can be 31.9999999)
804              
805 31         676 my $m2 = ($m <= 2);
806 31         189 $y -= $m2;
807 31         590 $m += $m2 * 12;
808              
809 31         1328 $rdate += floor(($m * 367 - 1094) / 12);
810 31         1636 $rdate += floor($y % 100 * $DAYS_PER_4_YEARS / 4);
811 31         1840 $rdate += floor($y / 100) * $DAYS_PER_100_YEARS + floor($y / 400);
812 31         783 $rdate -= $MAR_1_TO_DEC_31;
813 31         533 return $rdate;
814             }
815              
816             sub _ratadie2ymd {
817             # based on Rata Die calculation from https://metacpan.org/source/DROLSKY/DateTime-1.10/lib/DateTime.xs#L82
818 33     33   191 my $rdate = shift;
819              
820 33         297 my $d = floor($rdate);
821 33         111 $d += $MAR_1_TO_DEC_31;
822              
823 33         1499 my $c = floor((($d * 4) - 1) / $DAYS_PER_400_YEARS); # century
824 33         1110 $d -= floor($c * $DAYS_PER_400_YEARS / 4);
825 33         1480 my $y = floor((($d * 4) - 1) / $DAYS_PER_4_YEARS);
826 33         1056 $d -= floor($y * $DAYS_PER_4_YEARS / 4);
827 33         1399 my $m = floor((($d * 12) + 1093) / 367);
828 33         1312 $d -= floor((($m * 367) - 1094) / 12);
829 33         936 $y += ($c * 100);
830              
831 33         719 my $m12 = ($m > 12);
832 33         182 $y += $m12;
833 33         543 $m -= $m12 * 12;
834              
835 33         537 return ($y, $m, $d);
836             }
837              
838             sub _is_non_leap_year {
839 0     0     my $y = shift;
840 0           return (($y % 4)!=0) + (($y % 100)==0) - (($y % 400)==0);
841             }
842              
843             sub _days_in_year {
844 0     0     my $y = shift;
845 0           return 366 - _is_non_leap_year($y);
846             }
847              
848             sub _days_in_month {
849 0     0     my ($y, $m) = @_;
850 0           my $dec_simple = (2*($m==2) + ($m==4) + ($m==6) + ($m==9) + ($m==11));
851 0           my $dec_nonleap_feb = ($m==2) * _is_non_leap_year($y);
852 0           return 31 - $dec_simple - $dec_nonleap_feb;
853             }
854              
855             1;