File Coverage

blib/lib/DateTime/Format/Epoch.pm
Criterion Covered Total %
statement 98 103 95.1
branch 41 50 82.0
condition 9 15 60.0
subroutine 13 13 100.0
pod 3 3 100.0
total 164 184 89.1


line stmt bran cond sub pod time code
1             package DateTime::Format::Epoch;
2              
3 19     19   750531 use 5.00503; #qr
  19         44  
  19         521  
4 19     19   66 use strict;
  19         24  
  19         459  
5              
6 19     19   63 use vars qw($VERSION);
  19         24  
  19         812  
7              
8             $VERSION = '0.16';
9              
10 19     19   713 use DateTime 0.22;
  19         93601  
  19         336  
11 19     19   7225 use DateTime::LeapSecond;
  19         19360  
  19         511  
12              
13 19     19   15901 use Math::BigInt 'lib' => 'GMP,Pari,FastCalc';
  19         270889  
  19         320  
14 19     19   210434 use Params::Validate qw/validate BOOLEAN OBJECT/;
  19         34  
  19         13791  
15              
16             sub _floor {
17 11     11   132 my $x = shift;
18 11         14 my $ix = int $x;
19 11 50       27 if ($ix <= $x) {
20 11         47 return $ix;
21             } else {
22 0         0 return $ix - 1;
23             }
24             }
25              
26             my %units_per_second = (
27             seconds => 1,
28             milliseconds => 1000,
29             microseconds => 1e6,
30             nanoseconds => 1e9,
31             );
32              
33             sub new {
34 57     57 1 2025 my $class = shift;
35             my %p = validate( @_,
36             { epoch => {type => OBJECT,
37             can => 'utc_rd_values'},
38             unit => {callbacks =>
39             {'valid unit' =>
40 48 100   48   1546 sub { exists $units_per_second{$_[0]}
41             or $_[0] > 0 }},
42 57         1564 default => 'seconds'},
43             type => {regex => qr/^(?:int|float|bigint)$/,
44             default => 0},
45             local_epoch => {type => BOOLEAN,
46             default => 0},
47             dhms => {type => BOOLEAN,
48             default => 0},
49             skip_leap_seconds => {type => BOOLEAN,
50             default => 1},
51             start_at => {default => 0},
52             } );
53              
54 57 50       2017 $p{epoch} = $p{epoch}->clone if $p{epoch}->can('clone');
55              
56 57   66     666 $p{unit} = $units_per_second{$p{unit}} || $p{unit};
57 57 100       136 $p{unit} = 1 if $p{dhms};
58              
59 57 100       105 if (!$p{type}) {
60 11 100       36 $p{type} = ($p{unit} > 1e6 ? 'bigint' : 'int');
61             }
62              
63 57         130 ($p{epoch_rd_days}, $p{epoch_rd_secs}) = $p{epoch}->utc_rd_values;
64 57         384 $p{epoch_class} = ref $p{epoch};
65              
66 57 100       171 if (!$p{skip_leap_seconds}) {
67 5         158 $p{leap_secs} =
68             DateTime::LeapSecond::leap_seconds( $p{epoch_rd_days} );
69             }
70              
71 57         160 my $self = bless \%p, $class;
72 57         133 return $self;
73             }
74              
75             sub format_datetime {
76 96     96 1 76587 my ($self, $dt) = @_;
77              
78 96 100       207 unless (ref $self) {
79 25         132 $self = $self->new;
80             }
81              
82 96 100 66     406 $dt = $dt->clone->set_time_zone('floating')
      66        
      33        
      66        
83             if $self->{local_epoch} &&
84             $self->{epoch}->can('time_zone') &&
85             $self->{epoch}->time_zone->is_floating &&
86             $dt->can('time_zone') &&
87             !$dt->time_zone->is_floating;
88              
89 96         1002 my ($rd_days, $rd_secs) = $dt->utc_rd_values;
90 96         424 my $delta_days = $rd_days - $self->{epoch_rd_days};
91 96         105 my $delta_secs = $rd_secs - $self->{epoch_rd_secs};
92              
93 96         126 my $secs = $delta_days * 86_400 + $delta_secs;
94              
95 96 100       168 if (!$self->{skip_leap_seconds}) {
96 16         347 $secs += DateTime::LeapSecond::leap_seconds( $rd_days )
97             - $self->{leap_secs};
98             }
99              
100 96 100       260 if ($self->{type} eq 'bigint') {
101 21 100       43 if ($secs > 2_147_483_647) {
102 2         3 $secs = "$secs"; #https://rt.cpan.org/Ticket/Display.html?id=103517
103             }
104 21         71 $secs = Math::BigInt->new($secs);
105             }
106              
107 96         920 $secs *= $self->{unit};
108              
109 96 50       1962 if ($dt->can('nanosecond')) {
110 96         214 my $fraction = $dt->nanosecond / 1e9 * $self->{unit};
111 96 100       444 if ($self->{type} eq 'float') {
112 11         13 $secs += $fraction;
113             } else {
114 85         118 $secs += int $fraction;
115             }
116             }
117              
118 96         2101 $secs += $self->{start_at};
119              
120 96 100       1826 if ($self->{dhms}) {
121 5         5 my $mins = int($secs / 60);
122 5         6 $secs -= $mins * 60;
123 5         3 my $hours = int($mins / 60);
124 5         5 $mins -= $hours * 60;
125 5         22 my $days = int($hours / 24);
126 5         4 $hours -= $days * 24;
127              
128 5         27 return $days, $hours, $mins, $secs;
129             }
130              
131 91         400 return $secs;
132             }
133              
134             sub parse_datetime {
135 11     11 1 11649 my ($self, $str) = @_;
136              
137 11 100       25 unless (ref $self) {
138 7         15 $self = $self->new;
139             }
140              
141 11 50       24 if ($self->{dhms}) {
142 0         0 my (undef, $d, $h, $m, $s) = @_;
143 0         0 $str = (($d * 24 + $h) * 60 + $m) + $s;
144             }
145              
146 11         17 $str -= $self->{start_at};
147              
148 11         161 my $delta_days = _floor( $str / (86_400 * $self->{unit}) );
149 11         17 $str -= $delta_days * 86_400 * $self->{unit};
150              
151             # $str cannot be negative now, so int() instead of _floor()
152 11         212 my $delta_secs = int( $str / $self->{unit} );
153 11         92 $str -= $delta_secs * $self->{unit};
154              
155 11         143 my $delta_nano = $str / $self->{unit} * 1e9;
156              
157 11         159 my $rd_days = $self->{epoch_rd_days} + $delta_days;
158 11         86 my $rd_secs = $self->{epoch_rd_secs} + $delta_secs;
159              
160 11 100       85 if (!$self->{skip_leap_seconds}) {
161 1         22 $rd_secs -= DateTime::LeapSecond::leap_seconds( $rd_days )
162             - $self->{leap_secs};
163 1 50       393 if ($rd_secs >= DateTime::LeapSecond::day_length( $rd_days )) {
    0          
164 1         97 $rd_secs -= DateTime::LeapSecond::day_length( $rd_days );
165 1         132 $rd_days++;
166             } elsif ($rd_secs < 0) {
167 0         0 $rd_days--;
168 0         0 $rd_secs += DateTime::LeapSecond::day_length( $rd_days );
169             }
170             } else {
171 10 100       16 if ($rd_secs >= 86400) {
172 2         2 $rd_secs -= 86400;
173 2         2 $rd_days++;
174             }
175             }
176              
177 11 100       52 $rd_days = $rd_days->numify if UNIVERSAL::isa($rd_days, 'Math::BigInt');
178 11 100       54 $rd_secs = $rd_secs->numify if UNIVERSAL::isa($rd_secs, 'Math::BigInt');
179              
180 11         40 my $temp_dt = bless { rd_days => $rd_days, rd_secs => $rd_secs},
181             'DateTime::Format::Epoch::_DateTime';
182              
183 11         36 my $dt = $self->{epoch_class}->from_object( object => $temp_dt );
184              
185 11 50       2925 if (!$self->{local_epoch}) {
186 11 50       44 $dt->set_time_zone( 'UTC' ) if $dt->can('set_time_zone');
187             }
188              
189 11         893 return $dt;
190             }
191              
192             sub DateTime::Format::Epoch::_DateTime::utc_rd_values {
193 11     11   219 my $self = shift;
194              
195 11         40 return $self->{rd_days}, $self->{rd_secs};
196             }
197              
198             1;
199             __END__