File Coverage

blib/lib/Business/DK/CPR.pm
Criterion Covered Total %
statement 138 190 72.6
branch 27 58 46.5
condition 5 15 33.3
subroutine 34 34 100.0
pod 9 9 100.0
total 213 306 69.6


line stmt bran cond sub pod time code
1             package Business::DK::CPR;
2              
3 13     13   117149 use strict;
  13         40  
  13         389  
4 13     13   66 use warnings;
  13         26  
  13         364  
5 13     13   67 use Carp qw(croak carp);
  13         24  
  13         709  
6 13     13   6564 use Business::DK::CVR qw(_calculate_sum);
  13         209309  
  13         786  
7 13     13   6183 use Date::Calc qw(check_date);
  13         94638  
  13         1136  
8 13     13   100 use base 'Exporter';
  13         29  
  13         1247  
9 13     13   7492 use integer;
  13         197  
  13         69  
10 13     13   6880 use Tie::IxHash;
  13         32895  
  13         458  
11 13     13   93 use Readonly;
  13         33  
  13         786  
12 13     13   89 use Params::Validate qw( validate_pos SCALAR ARRAYREF );
  13         25  
  13         625  
13 13     13   224 use 5.010; #5.10.0
  13         50  
14              
15             our $VERSION = '0.15';
16             our @EXPORT_OK = qw(
17             validate
18             validateCPR
19             generate
20             validate1968
21             generate1968
22             validate2007
23             generate2007
24             calculate
25             );
26              
27 13     13   89 use constant MODULUS_OPERAND_1968 => 11;
  13         26  
  13         840  
28 13     13   86 use constant MODULUS_OPERAND_2007 => 6;
  13         192  
  13         754  
29 13     13   87 use constant DATE_LENGTH => 6;
  13         21  
  13         706  
30 13     13   103 use constant CONTROL_CIFER_LENGTH => 4;
  13         87  
  13         696  
31 13     13   92 use constant VALID => 1;
  13         66  
  13         797  
32 13     13   84 use constant VALID_MALE => 1;
  13         26  
  13         642  
33 13     13   94 use constant VALID_FEMALE => 2;
  13         58  
  13         706  
34 13     13   81 use constant INVALID => 0;
  13         24  
  13         707  
35 13     13   82 use constant FEMALE => 'female';
  13         24  
  13         697  
36 13     13   79 use constant MALE => 'male';
  13         25  
  13         25272  
37              
38             Readonly my @controlcifers => qw(4 3 2 7 6 5 4 3 2 1);
39              
40             my %female_seeds;
41             tie %female_seeds, 'Tie::IxHash',
42             4 => { max => 9994, min => 10 },
43             2 => { max => 9998, min => 8 },
44             6 => { max => 9996, min => 12 };
45              
46             my %male_seeds;
47             tie %male_seeds, 'Tie::IxHash',
48             1 => { max => 9997, min => 7 },
49             3 => { max => 9999, min => 9 },
50             5 => { max => 9995, min => 11 };
51              
52             sub merge {
53 23     23 1 42 my ( $left_hashref, $right_hashref ) = @_;
54              
55 23         37 my %hash = %{$right_hashref};
  23         90  
56              
57 23         934 foreach ( keys %{$left_hashref} ) {
  23         65  
58 69         858 $hash{$_} = $left_hashref->{$_};
59             }
60              
61 23         257 return \%hash;
62             }
63              
64             sub calculate {
65 4     4 1 3071 my ($birthdate) = @_;
66              
67 4         94 validate_pos( @_,
68             { type => SCALAR, callbacks => { 'date' => \&_checkdate } } );
69              
70 2         6 my @cprs;
71 2         9 for ( 1 .. 999 ) {
72 1998         5341 my $n = sprintf '%03s', $_;
73              
74             #From DK::Business::CVR
75 1998         5035 my $sum = _calculate_sum( ( $birthdate . $n ), \@controlcifers );
76 1998         175397 my $mod = $sum % MODULUS_OPERAND_1968;
77              
78 1998         3002 my $checkciffer = ( MODULUS_OPERAND_1968 - $mod );
79              
80 1998 100       3677 if ( $checkciffer < 10 ) {
81 1636         4136 push @cprs, ( $birthdate . $n . $checkciffer );
82             }
83             }
84              
85 2 100       7 if (wantarray) {
86 1         210 return @cprs;
87             }
88             else {
89 1         91 return scalar @cprs;
90             }
91             }
92              
93             sub validateCPR {
94              
95             #We postpone parameter validation
96 1     1 1 3 return validate(shift);
97             }
98              
99             sub _length {
100 861     861   4872 my ( $number, $length ) = @_;
101              
102 861 100       2012 if ( length($number) != $length ) {
103 2         24 croak "argument: $number has to be $length digits long";
104             }
105 859         1313 return 1;
106             }
107              
108             sub validate {
109 14     14 1 771 my ($controlnumber) = @_;
110              
111             #We postpone parameter validation
112              
113 14         22 my $rv;
114 14 100       29 if ( $rv = validate1968($controlnumber) ) {
115 2         10 return $rv;
116             }
117             else {
118 7         15 return validate2007($controlnumber);
119             }
120             }
121              
122             sub validate2007 {
123 29     29 1 782 my ($controlnumber) = @_;
124 29         463 validate_pos( @_, { type => SCALAR, regex => qr/^\d+$/ } );
125              
126 27         342 _checkdate( substr $controlnumber, 0, DATE_LENGTH );
127 23         63 _assert_controlnumber($controlnumber);
128              
129 23         55 my $control = substr $controlnumber, DATE_LENGTH, CONTROL_CIFER_LENGTH;
130              
131 23         54 my $remainder = $control % MODULUS_OPERAND_2007;
132              
133             #Our own merge, could be Hash::Merge's merge
134 23         36 my %seeds = %{ merge( \%male_seeds, \%female_seeds ) };
  23         55  
135              
136 23 100 100     176 if ( my $series = $seeds{$remainder} ) {
    100 66        
137 13 100       77 if ( $control < $seeds{$remainder}->{min} ) {
    50          
138 11         63 return INVALID;
139             }
140             elsif ( $control > $seeds{$remainder}->{max} ) {
141 0         0 return INVALID;
142             }
143             }
144             elsif ( ( $control == 0 or $control == 6 ) && $remainder == 0 ) {
145 4         25 return INVALID;
146             }
147              
148 8 100       34 if ( $female_seeds{$remainder} ) {
    100          
    50          
149 1         12 return VALID_FEMALE;
150             }
151             elsif ( $male_seeds{$remainder} ) {
152 1         20 return VALID_MALE;
153             }
154             elsif ( $remainder == 0 ) {
155 6 50       96 if ( _is_equal($control) ) {
156 6         40 return VALID_FEMALE;
157             }
158             else {
159 0         0 return VALID_MALE;
160             }
161             }
162             else {
163 0         0 return INVALID;
164             }
165             }
166              
167             sub validate1968 {
168 846     846 1 240465 my ($controlnumber) = @_;
169              
170 846         12446 validate_pos( @_, { type => SCALAR, regex => qr/^\d+$/ } );
171              
172 842         11348 _checkdate( substr $controlnumber, 0, DATE_LENGTH );
173 835         2207 _assert_controlnumber($controlnumber);
174              
175 835         2078 my $sum = _calculate_sum( $controlnumber, \@controlcifers );
176              
177             #Note this might look like it is turned upside down but no rest from the
178             #modulus calculation indicated validity
179 835 100       80455 if ( $sum % MODULUS_OPERAND_1968 ) {
180 9         32 return INVALID;
181             }
182             else {
183 826 100       1510 if ( _is_equal($sum) ) {
184 411         1377 return VALID_MALE;
185             }
186             else {
187 415         1402 return VALID_FEMALE;
188             }
189             }
190             }
191              
192             sub _is_equal {
193 832     832   1614 my ($operand) = @_;
194              
195 832         8406 validate_pos( @_, { type => SCALAR, regex => qr/^\d+$/ } );
196              
197 832         8954 return ( not( $operand % 2 ) );
198             }
199              
200             sub _assert_controlnumber {
201 863     863   2389 my ($controlnumber) = @_;
202              
203 863         8270 validate_pos( @_, { type => SCALAR, regex => qr/^\d+$/ } );
204              
205 861         8540 _length( $controlnumber, scalar @controlcifers );
206              
207 859         1321 return VALID;
208             }
209              
210             sub _checkdate {
211              
212 879     879   2473 my $dateregex = qr{
213             \A #beginning of line
214             (\d{2}) #day of month, 2 digit representation, 01-31
215             (\d{2}) #month, 2 digit representation jan 01 - dec 12
216             (\d{2}) #year, 2 digit representation
217             \Z #end of line
218             }xsm;
219              
220             #According to the documentation validate_pos gets two paramters, hence the
221             #second optional argument specification
222 879         8234 validate_pos(
223             @_,
224             { type => SCALAR, regex => $dateregex },
225             { type => ARRAYREF, optional => 1 },
226             );
227              
228             #Params::Validate does not capture for us, so we re-do our regex
229 870         9665 $_[0] =~ m/$dateregex/;
230              
231 870 100       4477 if ( not check_date( $3, $2, $1 ) ) {
232 9         113 croak "argument: $_[0] has to be a valid date in the format: ddmmyy";
233             }
234              
235 861         1927 return VALID;
236             }
237              
238             sub generate {
239 2     2 1 1597 my ( $birthdate, $gender ) = @_;
240              
241 2         63 validate_pos(
242             @_,
243             { type => SCALAR, callbacks => { 'date' => \&_checkdate }, },
244             { type => SCALAR, optional => 1, default => q{} },
245             );
246              
247 0         0 my @genders;
248              
249 0 0       0 if ($gender) {
250 0         0 push @genders, $gender;
251             }
252             else {
253 0         0 @genders = qw(male female);
254             }
255              
256 0         0 my %cprs;
257 0         0 foreach my $g (@genders) {
258 0         0 my @cprs2007 = generate2007( $birthdate, $g );
259              
260 0         0 my $i = 1;
261 0         0 foreach my $cpr (@cprs2007) {
262 0         0 $cprs{$cpr}++;
263             }
264             }
265              
266 0 0       0 if (wantarray) {
267 0         0 return keys %cprs;
268             }
269             else {
270 0         0 return scalar keys %cprs;
271             }
272             }
273              
274             sub generate2007 {
275 2     2 1 1549 my ( $birthdate, $gender ) = @_;
276              
277             #TODO assert gender?
278 2         66 validate_pos(
279             @_,
280             { type => SCALAR, callbacks => { 'date' => \&_checkdate }, },
281             { type => SCALAR, optional => 1 },
282             );
283              
284 0         0 my @cprs;
285             my %seeds;
286              
287 0 0       0 if ( defined $gender ) {
288 0 0       0 if ( $gender eq MALE ) {
    0          
289 0         0 %seeds = %male_seeds;
290             }
291             elsif ( $gender eq FEMALE ) {
292 0         0 %seeds = %female_seeds;
293             }
294             else {
295 0         0 carp("Unknown gender: $gender, assuming no gender");
296 0         0 $gender = undef;
297             }
298             }
299              
300 0 0       0 if ( not $gender ) {
301 0         0 %seeds = %{ merge( \%female_seeds, \%male_seeds ) };
  0         0  
302             }
303              
304 0         0 foreach my $seed ( keys %seeds ) {
305 0         0 my $s = $seeds{$seed}->{min};
306 0         0 while ( $s < $seeds{$seed}->{max} ) {
307 0         0 $s += MODULUS_OPERAND_2007;
308 0         0 push @cprs, ( $birthdate . sprintf '%04d', $s );
309             }
310             }
311              
312 0 0       0 if (wantarray) {
313 0         0 return @cprs;
314             }
315             else {
316 0         0 return scalar @cprs;
317             }
318             }
319              
320             sub generate1968 {
321 2     2 1 1589 my ( $birthdate, $gender ) = @_;
322              
323             #TODO assert gender?
324 2         67 validate_pos(
325             @_,
326             { type => SCALAR, callbacks => { 'date' => \&_checkdate }, },
327             { type => SCALAR, optional => 1, default => q{} },
328             );
329              
330 0           my @cprs;
331             my @malecprs;
332 0           my @femalecprs;
333              
334 0           my $checksum = 0;
335              
336 0           while ( $checksum < 9999 ) {
337              
338 0           my $cpr = $birthdate . sprintf '%04d', $checksum;
339              
340 0 0         if ( my $rv = validate1968($cpr) ) {
341              
342 0 0 0       if ( defined $gender and $rv ) {
343 0 0         if ( $rv == VALID_MALE ) {
    0          
344 0           push @malecprs, $cpr;
345             }
346             elsif ( $rv == VALID_FEMALE ) {
347 0           push @femalecprs, $cpr;
348             }
349              
350             }
351             else {
352 0           push @cprs, $cpr;
353             }
354             }
355 0           $checksum++;
356             }
357              
358 0 0 0       if ( $gender and $gender eq FEMALE ) {
    0 0        
359 0           @cprs = @femalecprs;
360             }
361             elsif ( $gender and $gender eq MALE ) {
362 0           @cprs = @malecprs;
363             }
364              
365 0 0         if (wantarray) {
366 0           return @cprs;
367             }
368             else {
369 0           return scalar @cprs;
370             }
371             }
372              
373             1;
374              
375             __END__