File Coverage

lib/SIRTX/Datecode.pm
Criterion Covered Total %
statement 97 190 51.0
branch 60 136 44.1
condition 13 32 40.6
subroutine 12 25 48.0
pod 17 17 100.0
total 199 400 49.7


line stmt bran cond sub pod time code
1             # Copyright (c) 2023-2026 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: module for interacting with SIRTX Datecodes
6              
7              
8             package SIRTX::Datecode;
9              
10 3     3   1056119 use v5.16;
  3         12  
11 3     3   16 use strict;
  3         5  
  3         91  
12 3     3   18 use warnings;
  3         10  
  3         208  
13              
14 3     3   16 use Carp;
  3         20  
  3         333  
15              
16 3     3   2377 use Data::Identifier;
  3         474497  
  3         23  
17              
18 3     3   282 use parent qw(Data::Identifier::Interface::Userdata Data::Identifier::Interface::Simple Data::Identifier::Interface::Subobjects);
  3         6  
  3         32  
19              
20             our $VERSION = v0.07;
21              
22              
23             sub new {
24 87     87 1 73867 my ($pkg, $type, $data, %opts) = @_;
25 87         172 my $code;
26             my $iso;
27              
28 87 50 33     550 croak 'Type or data not given' unless defined($type) && defined($data);
29              
30 87 100       227 if ($type eq 'from') {
31 1 50       2 if (eval {$data->isa('SIRTX::Datecode')}) {
  1 50       10  
    50          
    50          
    50          
    50          
    50          
32 0         0 $data = $data->datecode;
33 0         0 $type = 'datecode';
34 1 50       10 } elsif (eval {$data->isa('Data::Identifier') && $data->generator->eq('97b7f241-e1c5-4f02-ae3c-8e31e501e1dc')}) {
35 0         0 $data = $data->request;
36 0         0 $type = 'iso8601';
37 1         7 } elsif (eval {$data->isa('DateTime')}) {
38 0 0       0 if ($data->time_zone->isa('DateTime::TimeZone::Floating')) {
39 0         0 $data = $data->ymd;
40 0         0 $type = 'iso8601';
41             } else {
42 0         0 $data = $data->epoch;
43 0         0 $type = 'epoch';
44             }
45 1         15 } elsif (eval {$data->can('epoch')}) {
46 0         0 $data = $data->epoch;
47 0         0 $type = 'epoch';
48             } elsif ($data =~ /^[12][0-9]{3}(?:-[0-9]{2}(?:-[0-9]{2})?)?Z?$/) {
49 0         0 $type = 'iso8601';
50             } elsif ($data eq 'now') {
51 0         0 $data = time();
52 0         0 $type = 'epoch';
53             } elsif ($data eq 'null') {
54 1         2 $data = 0;
55 1         2 $type = 'datecode';
56             } else {
57 0         0 croak 'Invalid data: '.$data;
58             }
59             }
60              
61 87 100 66     1128 if ($type eq 'datecode' && $data =~ /^(0|[1-9][0-9]*)$/) {
    50 33        
    50 33        
62 15         22 $code = int $data;
63             } elsif ($type eq 'epoch' && $data =~ /^-?(0|[1-9][0-9]*)$/) {
64 0         0 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(int($data));
65 0         0 $iso = sprintf('%04u-%02u-%02uZ', $year + 1900, $mon, $mday);
66             } elsif ($type eq 'iso8601' && $data =~ /^[12][0-9]{3}(?:-[0-9]{2}(?:-[0-9]{2})?)?Z?$/) {
67 72         108 $iso = $data;
68             } else {
69 0         0 croak 'Invalid type/data: '.$type;
70             }
71              
72 87 50       206 croak 'Stray options passed' if scalar keys %opts;
73              
74 87   66     299 $code //= $pkg->_build_from_iso($iso);
75              
76 87 50       169 croak 'Invalid datecode value' if $code == 1;
77              
78 87         383 return bless({datecode => $code}, $pkg);
79             }
80              
81              
82             sub null {
83 1     1 1 693 my ($pkg, @opts) = @_;
84              
85 1 50       4 croak 'Stray options passed' if scalar @opts;
86              
87 1         7 return state $null = __PACKAGE__->new(from => 'null');
88             }
89              
90              
91             sub now {
92 0     0 1 0 my ($pkg, @opts) = @_;
93              
94 0         0 return $pkg->new(from => 'now', @opts);
95             }
96              
97              
98             sub datecode {
99 158     158 1 92942 my ($self, @opts) = @_;
100              
101 158 50       446 croak 'Stray options passed' if scalar @opts;
102              
103 158         549 return $self->{datecode};
104             }
105              
106              
107             sub iso8601 {
108 72     72 1 155 my ($self, @opts) = @_;
109 72         120 my $code = $self->{datecode};
110 72         191 my ($year, $month, $day);
111 72         0 my $utc;
112 72         0 my $iso;
113              
114 72 50       214 croak 'Stray options passed' if scalar @opts;
115              
116 72 50       155 return $self->{iso8601} if defined $self->{iso8601};
117              
118 72         123 $utc = $code & 1;
119 72         97 $code >>= 1;
120              
121 72 50       347 if ($code == 0) {
    100          
    100          
    100          
    100          
122 0         0 return undef;
123             } elsif ($code < 324) {
124 6         11 $year = 1582 + $code - 1;
125             } elsif ($code < 1299) {
126 18         29 $code -= 324;
127 18         46 $year = int($code / 13) + 1905;
128 18         32 $month = $code % 13;
129             } elsif ($code < 31714) {
130 24         39 $code -= 1299;
131 24         62 $year = int($code / 385) + 1980;
132 24         52 $code = $code % 385;
133 24 100       54 if ($code) {
134 18         26 $code -= 1;
135 18         33 $month = int($code / 32) + 1;
136 18         24 $day = $code % 32;
137             }
138             } elsif ($code < 32442) {
139 18         22 $code -= 31714;
140 18         31 $year = int($code / 13) + 2059;
141 18         23 $month = $code % 13;
142             } else {
143 6         19 $year = 2114 + $code - 32441;
144             }
145              
146 72 50       126 if ($year) {
147 72         108 $self->{year} = $year;
148 72         248 $iso = sprintf('%04u', $year);
149              
150 72 100       150 if ($month) {
151 40         77 $self->{month} = $month;
152 40         132 $iso .= sprintf('-%02u', $month);
153 40 100       114 if ($day) {
154 10         16 $self->{day} = $day;
155 10         25 $iso .= sprintf('-%02u', $day);
156             }
157             }
158              
159 72 100       139 $iso .= 'Z' if $utc;
160             }
161              
162 72         342 return $self->{iso8601} = $iso;
163             }
164              
165              
166             sub as {
167 0     0 1 0 my ($self, $as, %opts) = @_;
168              
169 0 0       0 croak 'No as given' unless defined $as;
170              
171 0 0       0 return $self if $as eq 'SIRTX::Datecode';
172              
173 0 0       0 croak 'Not supported: This is a null value' if $self->is_null;
174              
175 0 0       0 if ($as eq 'DateTime') {
176 0         0 my $year = $self->year;
177 0         0 my $month = $self->month;
178 0         0 my $day = $self->day;
179              
180 0         0 require DateTime;
181              
182 0 0       0 return DateTime->new(year => $year, month => $month, day => $day, ($self->is_utc ? (time_zone => 'UTC') : ()));
183             }
184              
185 0 0       0 croak 'Not supported: This is not in UTC' unless $self->is_utc;
186              
187 0 0       0 unless (defined $self->{'Data::Identifier'}) {
188 0         0 require Data::Identifier::Generate;
189 0         0 $self->{'Data::Identifier'} = Data::Identifier::Generate->date($self->iso8601);
190             }
191              
192 0 0 0     0 return $self->{'Data::Identifier'} if $as eq 'Data::Identifier' && scalar(keys %opts) == 0;
193              
194 0         0 return $self->{'Data::Identifier'}->as($as, %opts);
195             }
196              
197              
198             sub eq {
199 0     0 1 0 my ($self, $other, @opts) = @_;
200              
201 0 0       0 croak 'Stray options passed' if scalar @opts;
202              
203 0         0 foreach my $dc ($self, $other) {
204 0 0       0 if (!defined($dc)) {
    0          
205 0         0 $dc = __PACKAGE__->null;
206 0         0 } elsif (!eval {$dc->isa(__PACKAGE__)}) {
207 0         0 $dc = __PACKAGE__->new(from => $dc);
208             }
209             }
210              
211 0         0 return $self->datecode == $other->datecode;
212             }
213              
214              
215             sub cmp {
216 0     0 1 0 my ($self, $other, @opts) = @_;
217              
218 0 0       0 croak 'Stray options passed' if scalar @opts;
219              
220 0         0 foreach my $dc ($self, $other) {
221 0 0       0 if (!defined($dc)) {
    0          
222 0         0 $dc = __PACKAGE__->null;
223 0         0 } elsif (!eval {$dc->isa(__PACKAGE__)}) {
224 0         0 $dc = __PACKAGE__->new(from => $dc);
225             }
226             }
227              
228 0         0 return $self->datecode <=> $other->datecode;
229             }
230              
231              
232             sub is_utc {
233 0     0 1 0 my ($self, @opts) = @_;
234              
235 0 0       0 croak 'Stray options passed' if scalar @opts;
236              
237 0         0 return $self->{datecode} & 1;
238             }
239              
240              
241             sub is_floating {
242 0     0 1 0 my ($self, @opts) = @_;
243 0         0 return !$self->is_utc(@opts);
244             }
245              
246              
247             sub is_null {
248 1     1 1 515 my ($self, @opts) = @_;
249              
250 1 50       4 croak 'Stray options passed' if scalar @opts;
251              
252 1         14 return $self->{datecode} == 0;
253             }
254              
255              
256             sub assert_16bit {
257 0     0 1 0 my ($self, @opts) = @_;
258              
259 0 0       0 croak 'Stray options passed' if scalar @opts;
260              
261 0 0 0     0 unless ($self->{datecode} >= 0 && $self->{datecode} <= 0xFFFF) {
262 0         0 croak 'Datecode is outside of 16 bit range';
263             }
264              
265 0         0 return $self;
266             }
267              
268              
269             sub precision {
270 0     0 1 0 my ($self, @opts) = @_;
271 0   0     0 my $iso_len = length($self->iso8601 // '');
272              
273 0 0       0 croak 'Stray options passed' if scalar @opts;
274              
275 0 0       0 if ($iso_len <= 1) {
    0          
    0          
    0          
276 0         0 return 'null';
277             } elsif ($iso_len <= 5) {
278 0         0 return 'year';
279             } elsif ($iso_len <= 8) {
280 0         0 return 'month';
281             } elsif ($iso_len <= 11) {
282 0         0 return 'day';
283             }
284              
285 0         0 croak 'Invalid object state';
286             }
287              
288              
289             sub year {
290 0     0 1 0 my ($self, @args) = @_;
291 0         0 return $self->_get_part('year', @args);
292             }
293              
294             sub month {
295 0     0 1 0 my ($self, @args) = @_;
296 0         0 return $self->_get_part('month', @args);
297             }
298              
299             sub day {
300 0     0 1 0 my ($self, @args) = @_;
301 0         0 return $self->_get_part('day', @args);
302             }
303              
304             # ---- Implementation of Data::Identifier::Interface::Simple ----
305              
306             sub displayname {
307 0     0 1 0 my ($self, $part, %opts) = @_;
308              
309             # compatibility
310 0         0 delete $opts{default};
311 0         0 delete $opts{no_defaults};
312              
313 0 0       0 croak 'Stray options passed' if scalar keys %opts;
314              
315 0         0 return $self->iso8601;
316             }
317              
318             # ---- Private helpers ----
319              
320             sub _build_from_iso {
321 72     72   126 my ($pkg, $iso) = @_;
322 72         660 my ($year, $month, $day, $utc) = $iso =~ /^([12][0-9]{3})(?:-([0-9]{2})(?:-([0-9]{2}))?)?(Z?)$/;
323 72         134 my $code;
324              
325 72         147 $year = int $year;
326 72 100 66     266 $month = defined($month) && length($month) ? int($month) : 0;
327 72 100 66     201 $day = defined($day) && length($day) ? int($day) : 0;
328              
329 72 100       152 $day = 0 unless $month;
330              
331 72 50 66     353 if ($year < 1582) {
    100          
    100          
    100          
    100          
332 0         0 return undef;
333             } elsif ($year >= 1582 && $year <= 1904) {
334 6         12 $code = $year - 1582 + 1;
335             } elsif ($year <= 1979) {
336 18         42 $code = 324 + ($year - 1905) * 13 + $month;
337             } elsif ($year <= 2058) {
338 24 100       78 $code = 1299 + ($year - 1980) * 385 + ($month ? 1 + ($month - 1) * 32 : 0) + $day;
339             } elsif ($year <= 2114) {
340 18         31 $code = 31714 + ($year - 2059) * 13 + $month;
341             } else {
342 6         9 $code = 32441 + $year - 2114;
343             }
344              
345 72 50       123 if (defined $code) {
346 72         99 $code *= 2;
347 72 100       149 $code |= $utc ? 1 : 0;
348             }
349              
350 72         219 return $code;
351             }
352              
353             sub _get_part {
354 0     0     my ($self, $part, %opts) = @_;
355 0           my $has_default = exists $opts{default};
356 0           my $default = delete $opts{default};
357              
358 0           delete $opts{no_defaults}; # compatibility
359              
360 0 0         croak 'Stray options passed' if scalar keys %opts;
361              
362 0           $self->iso8601; # fill cache
363              
364 0 0         return $self->{$part} if defined $self->{$part};
365              
366 0 0         return $default if $has_default;
367              
368 0           croak 'Value not available: '.$part;
369             }
370              
371             1;
372              
373             __END__