File Coverage

blib/lib/PDL/DateTime.pm
Criterion Covered Total %
statement 340 463 73.4
branch 146 286 51.0
condition 11 63 17.4
subroutine 61 80 76.2
pod 40 44 90.9
total 598 936 63.8


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