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.003002'; # VERSION
8              
9 3     3   17577 use v5.10;
  3         13  
10 3     3   23 use strict;
  3         7  
  3         74  
11 3     3   18 use warnings;
  3         11  
  3         109  
12 3     3   1697 use parent qw(Exporter);
  3         1088  
  3         17  
13 3     3   1792 use Hash::MostUtils qw(hashmap);
  3         17098  
  3         226  
14 3     3   3636 use Math::BigInt try => 'GMP';
  3         84484  
  3         18  
15 3     3   87106 use Math::BigFloat;
  3         93047  
  3         24  
16 3     3   2590 use Scalar::Util qw(looks_like_number);
  3         9  
  3         212  
17 3     3   1924 use Time::Moment;
  3         4424  
  3         6482  
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 197 my $num = shift;
61 1         4 _epoch2time($num, 1_000_000_000);
62             }
63             sub to_apfs {
64 1     1 0 1771 my $tm = shift;
65 1         6 _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 4678 my $num = shift;
75 3         14 _epoch2time($num, 1_000_000, -11_644_473_600);
76             }
77             sub to_chrome {
78 1     1 0 1013 my $tm = shift;
79 1         9 _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 4375 my $num = shift;
87 2         9 _epoch2time($num, 1, 978_307_200);
88             }
89             sub to_cocoa {
90 1     1 0 1001 my $tm = shift;
91 1         6 _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 2821 my $num = shift;
99              
100 1         5 my $year = ($num >> 25) & 0b1111111;
101              
102 1         5 my $month = ($num >> 21) & 0b1111;
103 1 50 33     11 return if $month < 1 or $month > 12;
104              
105 1         3 my $day = ($num >> 16) & 0b11111;
106 1 50 33     9 return if $day < 1 or $day > 31;
107              
108 1         4 my $hour = ($num >> 11) & 0b11111;
109 1 50 33     9 return if $hour < 0 or $hour > 23;
110              
111 1         4 my $minute = ($num >> 5) & 0b111111;
112 1 50 33     10 return if $minute < 0 or $minute > 60;
113              
114 1         4 my $second = ($num ) & 0b11111;
115 1 50 33     10 return if $second < 0 or $second > 60;
116              
117 1         15 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 932 my $tm = shift;
129              
130 1 50       11 if (ref $tm ne 'Time::Moment') {
131 1         10 $tm = Time::Moment->from_string($tm);
132             }
133              
134 1         23 ($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 863 my $n = shift;
148              
149 1 50       8 return unless looks_like_number $n;
150              
151 1         7 my $b = Math::BigInt->new($n);
152 1         165 my($total_days, $seconds) = $b->bdiv($SECONDS_PER_DAY);
153 1         438 my($months, $days) = $total_days->bdiv(32);
154              
155 1 50 33     351 return if $months < $MIN_UNIT_MONTHS
156             or $months > $MAX_UNIT_MONTHS;
157              
158 1         392 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 1030 my $tm = shift;
166              
167 1 50       7 if (ref $tm ne 'Time::Moment') {
168 1         9 $tm = Time::Moment->from_string($tm);
169             }
170              
171 1         17 ((((($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 6   50 6 1 3628 my $days = shift // return;
185              
186 6 50       29 return unless looks_like_number $days;
187              
188 6         34 my $t = Time::Moment->from_epoch(-2_209_161_600);
189              
190 6         19 my $intdays = int($days);
191              
192 6 50 33     35 return if $intdays < $MIN_UNIT_DAYS
193             or $intdays > $MAX_UNIT_DAYS;
194              
195             # Want the fractional part of the day in nanoseconds.
196 6         19 my $fracday = int(($days - $intdays) * $NANOSECONDS_PER_DAY);
197              
198 6         48 return $t->plus_days($intdays)->plus_nanoseconds($fracday);
199             }
200             sub to_icq {
201 3     3 0 1493 my $tm = shift;
202              
203 3 50       29 if (ref $tm ne 'Time::Moment') {
204 3         23 $tm = Time::Moment->from_string($tm);
205             }
206              
207 3         15 my $t2 = Time::Moment->from_epoch(-2_209_161_600);
208              
209 3         19 $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 1612 my $num = shift;
216 2         9 _epoch2time($num, 1000);
217             }
218             sub to_java {
219 1     1 0 857 my $tm = shift;
220 1         11 _time2epoch($tm, 1000);
221             }
222              
223              
224             # Mozilla time is in microseconds since the Unix epoch.
225             sub mozilla {
226 1     1 1 2247 my $num = shift;
227 1         6 _epoch2time($num, 1_000_000);
228             }
229             sub to_mozilla {
230 1     1 0 876 my $tm = shift;
231 1         5 _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 2843 my $bytes = shift // return;
239              
240 2 50       10 my $d_days = unpack('d', $bytes) or return;
241              
242 2 50       18 return if $d_days eq '-nan';
243              
244 2         7 return icq $d_days;
245             }
246             sub to_ole {
247 1   50 1 0 928 my $t = shift // return;
248              
249 1         4 my $icq = to_icq($t);
250              
251 1 50       7 my $epoch = pack('d', $icq) or return;
252              
253 1         4 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 727 my $num = shift;
261 1         5 _epoch2time($num, 1_000_000, -62_167_219_200);
262             }
263             sub to_symbian {
264 1     1 0 893 my $tm = shift;
265 1         4 _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 3526 my $num = shift;
272 2         7 _epoch2time($num);
273             }
274             sub to_unix {
275 2     2 0 1951 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 1912 my $num = shift;
285 2         6 _epoch2time($num, 10_000_000, -12_219_292_800);
286             }
287             sub to_uuid_v1 {
288 2     2 0 2307 my $tm = shift;
289 2         7 _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 2329 my $num = shift;
298 2         6 _epoch2time($num, 10_000_000, -62_135_596_800);
299             }
300             sub to_windows_date {
301 1     1 0 575 my $tm = shift;
302 1         4 _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 2381 my $num = shift;
311 2         7 _epoch2time($num, 10_000_000, -11_644_473_600);
312             }
313             sub to_windows_file {
314 1     1 0 654 my $tm = shift;
315 1         4 _time2epoch($tm, 10_000_000, -11_644_473_600);
316             }
317              
318              
319             sub windows_system {
320 1     1 1 1959 my $num = shift;
321              
322 1 50       6 if ($num =~ /^[0-9a-fA-F]{32}$/) {
323 1         4 $num = "0x$num";
324             }
325              
326 1         8 my $bigint = Math::BigInt->new($num);
327 1 50       462 return if $bigint eq 'NaN';
328              
329 1         37 my $hex = substr $bigint->as_hex, 2;
330            
331 1 50       377 return if length $hex > 32;
332 1 50       4 return if length $hex < 0;
333 1         3 $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         300  
338            
339 1         17 my %wst;
340 1         6 @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     53 $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         16 nanosecond => $wst{milliseconds} * 1e6);
360             }
361              
362             sub to_windows_system {
363 1     1 0 593 my $tm = shift;
364 1         8 $tm = Time::Moment->from_string($tm);
365            
366             return unless
367 1 50 33     56 $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         14 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   13 join '', hashmap {"$b$a"} ($hex =~ /../g);
  8         299  
388             }
389            
390             sub _epoch2time {
391 18   50 18   63 my $num = shift // return;
392 18   100     56 my $q = shift // 1;
393 18   100     61 my $s = shift // 0;
394              
395 18 100       110 return unless looks_like_number $num;
396              
397 17         77 my($z, $m) = Math::BigInt->new($num)->bdiv($q);
398 17         6775 my $seconds = $z + $s;
399              
400 17 50 33     4213 return if $seconds < $MIN_SECONDS or $seconds > $MAX_SECONDS;
401              
402 17         4964 my $nanoseconds = ($m * 1e9)->bdiv($q);
403              
404 17         7547 Time::Moment->from_epoch($seconds, $nanoseconds);
405             }
406              
407             sub _time2epoch {
408 12   50 12   45 my $t = shift // return;
409 12   100     39 my $m = shift // 1;
410 12   100     42 my $s = shift // 0;
411              
412 12 50       55 if (ref $t ne 'Time::Moment') {
413 12         97 $t = Time::Moment->from_string($t);
414             }
415            
416 12         83 my $bf = Math::BigFloat->new($t->nanosecond)->bdiv(1e9);
417 12         8948 int $m*($t->epoch + $bf - $s);
418             }
419              
420             1;
421              
422             __END__