File Coverage

blib/lib/Time/Moment/Epoch.pm
Criterion Covered Total %
statement 153 153 100.0
branch 25 48 52.0
condition 51 132 38.6
subroutine 43 43 100.0
pod 15 30 50.0
total 287 406 70.6


line stmt bran cond sub pod time code
1             #####################################################################
2             ## ABSTRACT: Convert various epoch times to Time::Moment times.
3             #####################################################################
4              
5              
6             package Time::Moment::Epoch;
7             our $VERSION = '1.003003'; # VERSION
8              
9 3     3   10947 use v5.10;
  3         20  
10 3     3   15 use strict;
  3         12  
  3         53  
11 3     3   12 use warnings;
  3         6  
  3         88  
12 3     3   1048 use parent qw(Exporter);
  3         753  
  3         13  
13 3     3   1241 use Hash::MostUtils qw(hashmap);
  3         11843  
  3         154  
14 3     3   2689 use Math::BigInt try => 'GMP';
  3         65851  
  3         11  
15 3     3   60237 use Math::BigFloat;
  3         66150  
  3         14  
16 3     3   1572 use Scalar::Util qw(looks_like_number);
  3         6  
  3         115  
17 3     3   1174 use Time::Moment;
  3         3948  
  3         5382  
18              
19             my $SECONDS_PER_DAY = 24 * 60 * 60;
20             my $NANOSECONDS_PER_DAY = $SECONDS_PER_DAY * 1e9;
21              
22             # Time::Moment can represent all epoch integers from -62,135,596,800
23             # to 253,402,300,799; this range suffices to measure times to
24             # nanosecond precision for any instant that is within
25             # 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z.
26             my $MAX_SECONDS = 253_402_300_799;
27             my $MIN_SECONDS = -62_135_596_800;
28              
29             # Here are a few more constants from moment.h that we need.
30             my $MAX_UNIT_DAYS = 3652425;
31             my $MIN_UNIT_DAYS = -3652425;
32             my $MAX_UNIT_MONTHS = 120000;
33             my $MIN_UNIT_MONTHS = -120000;
34              
35             our @conversions = qw(
36             apfs
37             chrome
38             cocoa
39             dos
40             google_calendar
41             icq
42             java
43             mozilla
44             ole
45             symbian
46             unix
47             uuid_v1
48             windows_date
49             windows_file
50             windows_system
51             );
52             our @to_conversions = map {"to_$_"} @conversions;
53             our @EXPORT_OK = (@conversions, @to_conversions,
54             qw(@conversions @to_conversions));
55             our %EXPORT_TAGS = (all => [@EXPORT_OK]);
56              
57              
58             # APFS time is in nanoseconds since the Unix epoch.
59             sub apfs {
60 1     1 1 128 my $num = shift;
61 1         3 _epoch2time($num, 1_000_000_000);
62             }
63             sub to_apfs {
64 1     1 0 716 my $tm = shift;
65 1         3 _time2epoch($tm, 1_000_000_000);
66             }
67              
68              
69              
70             # Chrome time is the number of microseconds since 1601-01-01, which is
71             # 11,644,473,600 seconds before the Unix epoch.
72             #
73             sub chrome {
74 3     3 1 2180 my $num = shift;
75 3         8 _epoch2time($num, 1_000_000, -11_644_473_600);
76             }
77             sub to_chrome {
78 1     1 0 484 my $tm = shift;
79 1         4 _time2epoch($tm, 1_000_000, -11_644_473_600);
80             }
81              
82              
83             # Cocoa time is the number of seconds since 2001-01-01, which
84             # is 978,307,200 seconds after the Unix epoch.
85             sub cocoa {
86 2     2 1 2046 my $num = shift;
87 2         5 _epoch2time($num, 1, 978_307_200);
88             }
89             sub to_cocoa {
90 1     1 0 482 my $tm = shift;
91 1         4 _time2epoch($tm, 1, 978_307_200);
92             }
93              
94              
95             # DOS time uses bit fields to store dates between 1980-01-01 and
96             # 2107-12-31 (it fails outside that range).
97             sub dos {
98 1     1 1 1371 my $num = shift;
99              
100 1         3 my $year = ($num >> 25) & 0b1111111;
101              
102 1         3 my $month = ($num >> 21) & 0b1111;
103 1 50 33     6 return if $month < 1 or $month > 12;
104              
105 1         2 my $day = ($num >> 16) & 0b11111;
106 1 50 33     5 return if $day < 1 or $day > 31;
107              
108 1         1 my $hour = ($num >> 11) & 0b11111;
109 1 50 33     4 return if $hour < 0 or $hour > 23;
110              
111 1         2 my $minute = ($num >> 5) & 0b111111;
112 1 50 33     5 return if $minute < 0 or $minute > 60;
113              
114 1         2 my $second = ($num ) & 0b11111;
115 1 50 33     3 return if $second < 0 or $second > 60;
116              
117 1         19 Time::Moment->new(
118             year => 1980 + $year,
119             month => $month,
120             day => $day,
121             hour => $hour,
122             minute => $minute,
123             second => 2 * $second,
124             );
125              
126             }
127             sub to_dos {
128 1     1 0 455 my $tm = shift;
129              
130 1 50       5 if (ref $tm ne 'Time::Moment') {
131 1         5 $tm = Time::Moment->from_string($tm);
132             }
133              
134 1         13 ($tm->year - 1980 << 25) +
135             ($tm->month << 21) +
136             ($tm->day_of_month << 16) +
137             ($tm->hour << 11) +
138             ($tm->minute << 5) +
139             ($tm->second / 2);
140              
141             }
142              
143              
144             # Google Calendar time seems to count 32-day months from the day
145             # before the Unix epoch. @noppers worked out how to do this.
146             sub google_calendar {
147 1     1 1 439 my $n = shift;
148              
149 1 50       4 return unless looks_like_number $n;
150              
151 1         5 my $b = Math::BigInt->new($n);
152 1         49 my($total_days, $seconds) = $b->bdiv($SECONDS_PER_DAY);
153 1         220 my($months, $days) = $total_days->bdiv(32);
154              
155 1 50 33     177 return if $months < $MIN_UNIT_MONTHS
156             or $months > $MAX_UNIT_MONTHS;
157              
158 1         185 Time::Moment
159             ->from_epoch(-$SECONDS_PER_DAY)
160             ->plus_days($days)
161             ->plus_months($months)
162             ->plus_seconds($seconds);
163             }
164             sub to_google_calendar {
165 1     1 0 493 my $tm = shift;
166              
167 1 50       4 if (ref $tm ne 'Time::Moment') {
168 1         6 $tm = Time::Moment->from_string($tm);
169             }
170              
171 1         16 ((((($tm->year - 1970 )*12
172             + ($tm->month - 1))*32
173             + $tm->day_of_month)*24
174             + $tm->hour )*60
175             + $tm->minute )*60
176             + $tm->second;
177             }
178              
179              
180             # ICQ time is the number of days since 1899-12-30, which is
181             # 2,209,161,600 seconds before the Unix epoch. Days can have a
182             # fractional part.
183             sub icq {
184 4   50 4 1 883 my $days = shift // return;
185              
186 4 50       12 return unless looks_like_number $days;
187              
188 4         16 my $t = Time::Moment->from_epoch(-2_209_161_600);
189              
190 4         6 my $intdays = int($days);
191              
192 4 50 33     17 return if $intdays < $MIN_UNIT_DAYS
193             or $intdays > $MAX_UNIT_DAYS;
194              
195             # Want the fractional part of the day in nanoseconds.
196 4         7 my $fracday = int(($days - $intdays) * $NANOSECONDS_PER_DAY);
197              
198 4         18 return $t->plus_days($intdays)->plus_nanoseconds($fracday);
199             }
200             sub to_icq {
201 2     2 0 425 my $tm = shift;
202              
203 2 50       6 if (ref $tm ne 'Time::Moment') {
204 2         11 $tm = Time::Moment->from_string($tm);
205             }
206              
207 2         6 my $t2 = Time::Moment->from_epoch(-2_209_161_600);
208              
209 2         9 $t2->delta_nanoseconds($tm) / $NANOSECONDS_PER_DAY;
210             }
211              
212              
213             # Java time is in milliseconds since the Unix epoch.
214             sub java {
215 2     2 1 979 my $num = shift;
216 2         6 _epoch2time($num, 1000);
217             }
218             sub to_java {
219 1     1 0 482 my $tm = shift;
220 1         3 _time2epoch($tm, 1000);
221             }
222              
223              
224             # Mozilla time is in microseconds since the Unix epoch.
225             sub mozilla {
226 1     1 1 1285 my $num = shift;
227 1         4 _epoch2time($num, 1_000_000);
228             }
229             sub to_mozilla {
230 1     1 0 485 my $tm = shift;
231 1         3 _time2epoch($tm, 1_000_000);
232             }
233              
234              
235             # OLE time is the number of days since 1899-12-30, which is
236             # 2,209,161,600 seconds before the Unix epoch.
237             sub ole {
238 2   50 2 1 1710 my $bytes = shift // return;
239              
240 2 50       8 my $d_days = unpack('d', $bytes) or return;
241              
242 2 50       13 return if $d_days eq '-nan';
243              
244 2         5 return icq $d_days;
245             }
246             sub to_ole {
247 1   50 1 0 432 my $t = shift // return;
248              
249 1         3 my $icq = to_icq($t);
250              
251 1 50       7 my $epoch = pack('d', $icq) or return;
252              
253 1         2 return $epoch;
254             }
255              
256              
257             # Symbian time is the number of microseconds since the year 0, which
258             # is 62,167,219,200 seconds before the Unix epoch.
259             sub symbian {
260 1     1 1 433 my $num = shift;
261 1         3 _epoch2time($num, 1_000_000, -62_167_219_200);
262             }
263             sub to_symbian {
264 1     1 0 481 my $tm = shift;
265 1         3 _time2epoch($tm, 1_000_000, -62_167_219_200);
266             }
267              
268              
269             # Unix time is the number of seconds since 1970-01-01.
270             sub unix {
271 2     2 1 2052 my $num = shift;
272 2         6 _epoch2time($num);
273             }
274             sub to_unix {
275 2     2 0 1658 my $tm = shift;
276 2         5 _time2epoch($tm);
277             }
278              
279              
280             # UUID version 1 time (RFC 4122) is the number of hectonanoseconds
281             # (100 ns) since 1582-10-15, which is 12,219,292,800 seconds before
282             # the Unix epoch.
283             sub uuid_v1 {
284 2     2 1 1704 my $num = shift;
285 2         5 _epoch2time($num, 10_000_000, -12_219_292_800);
286             }
287             sub to_uuid_v1 {
288 2     2 0 1996 my $tm = shift;
289 2         5 _time2epoch($tm, 10_000_000, -12_219_292_800);
290             }
291              
292              
293             # Windows date time (e.g., .NET) is the number of hectonanoseconds
294             # (100 ns) since 0001-01-01, which is 62,135,596,800 seconds before
295             # the Unix epoch.
296             sub windows_date {
297 2     2 1 2042 my $num = shift;
298 2         5 _epoch2time($num, 10_000_000, -62_135_596_800);
299             }
300             sub to_windows_date {
301 1     1 0 488 my $tm = shift;
302 1         3 _time2epoch($tm, 10_000_000, -62_135_596_800);
303             }
304              
305              
306             # Windows file time (e.g., NTFS) is the number of hectonanoseconds
307             # (100 ns) since 1601-01-01, which is 11,644,473,600 seconds before
308             # the Unix epoch.
309             sub windows_file {
310 2     2 1 2045 my $num = shift;
311 2         5 _epoch2time($num, 10_000_000, -11_644_473_600);
312             }
313             sub to_windows_file {
314 1     1 0 488 my $tm = shift;
315 1         3 _time2epoch($tm, 10_000_000, -11_644_473_600);
316             }
317              
318              
319             sub windows_system {
320 1     1 1 1516 my $num = shift;
321              
322 1 50       7 if ($num =~ /^[0-9a-fA-F]{32}$/) {
323 1         3 $num = "0x$num";
324             }
325              
326 1         5 my $bigint = Math::BigInt->new($num);
327 1 50       430 return if $bigint eq 'NaN';
328              
329 1         36 my $hex = substr $bigint->as_hex, 2;
330              
331 1 50       364 return if length $hex > 32;
332 1 50       3 return if length $hex < 0;
333 1         4 $hex = "0$hex" while length $hex < 32;
334              
335 1         9 my @bytes = ($hex =~ /../g);
336 1         5 my @keys = qw(year month day_of_week day hour minute second milliseconds);
337 1     8   8 my @values = hashmap {hex "$b$a"} @bytes;
  8         223  
338              
339 1         14 my %wst;
340 1         7 @wst{@keys} = @values;
341              
342             return unless
343             $wst{year} >= 1601 and $wst{year} <= 30827 and
344             $wst{month} >= 1 and $wst{month} <= 12 and
345             $wst{day_of_week} >= 0 and $wst{day_of_week} <= 6 and
346             $wst{day} >= 1 and $wst{day} <= 31 and
347             $wst{hour} >= 0 and $wst{hour} <= 23 and
348             $wst{minute} >= 0 and $wst{minute} <= 59 and
349             $wst{second} >= 0 and $wst{second} <= 59 and
350 1 50 33     56 $wst{milliseconds} >= 0 and $wst{milliseconds} <= 999;
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
351              
352             return Time::Moment->new(
353             year => $wst{year},
354             month => $wst{month},
355             day => $wst{day},
356             hour => $wst{hour},
357             minute => $wst{minute},
358             second => $wst{second},
359 1         15 nanosecond => $wst{milliseconds} * 1e6);
360             }
361              
362             sub to_windows_system {
363 1     1 0 476 my $tm = shift;
364 1         5 $tm = Time::Moment->from_string($tm);
365            
366             return unless
367 1 50 33     69 $tm->year >= 1601 and $tm->year <= 30827 and
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
368             $tm->month >= 1 and $tm->month <= 12 and
369             $tm->day_of_week >= 1 and $tm->day_of_week <= 7 and
370             $tm->day_of_month >= 1 and $tm->day_of_month <= 31 and
371             $tm->hour >= 0 and $tm->hour <= 23 and
372             $tm->minute >= 0 and $tm->minute <= 59 and
373             $tm->second >= 0 and $tm->second <= 59 and
374             $tm->millisecond >= 0 and $tm->millisecond <= 999;
375              
376 1         16 my $hex = sprintf "%04x%04x%04x%04x%04x%04x%04x%04x",
377             $tm->year,
378             $tm->month,
379             $tm->day_of_week % 7,
380             $tm->day_of_month,
381             $tm->hour,
382             $tm->minute,
383             $tm->second,
384             $tm->millisecond;
385              
386             # Change endian-ness.
387 1     8   23 join '', hashmap {"$b$a"} ($hex =~ /../g);
  8         186  
388             }
389              
390             sub _epoch2time {
391 18   50 18   42 my $num = shift // return;
392 18   100     37 my $q = shift // 1;
393 18   100     33 my $s = shift // 0;
394              
395 18 100       54 return unless looks_like_number $num;
396              
397 17         51 my($z, $m) = Math::BigInt->new($num)->bdiv($q);
398 17         4481 my $seconds = $z + $s;
399              
400 17 50 33     2675 return if $seconds < $MIN_SECONDS or $seconds > $MAX_SECONDS;
401              
402 17         3070 my $nanoseconds = ($m * 1e9)->bdiv($q);
403              
404 17         5133 Time::Moment->from_epoch($seconds, $nanoseconds);
405             }
406              
407             sub _time2epoch {
408 12   50 12   31 my $t = shift // return;
409 12   100     27 my $m = shift // 1;
410 12   100     28 my $s = shift // 0;
411              
412 12 50       26 if (ref $t ne 'Time::Moment') {
413 12         59 $t = Time::Moment->from_string($t);
414             }
415              
416 12         47 my $bf = Math::BigFloat->new($t->nanosecond)->bdiv(1e9);
417 12         6144 int $m*($t->epoch + $bf - $s);
418             }
419              
420             1;
421              
422             __END__