File Coverage

blib/lib/Business/HK/IdentityCard.pm
Criterion Covered Total %
statement 47 47 100.0
branch 12 12 100.0
condition 5 6 83.3
subroutine 11 11 100.0
pod 4 4 100.0
total 79 80 98.7


line stmt bran cond sub pod time code
1 1     1   25847 use strict;
  1         3  
  1         44  
2 1     1   14 use warnings;
  1         4  
  1         831  
3             package Business::HK::IdentityCard;
4             our $VERSION = '1.000'; # VERSION
5              
6             # ABSTRACT: validate identity card numbers used in Hong Kong
7              
8              
9              
10             sub new
11             {
12 14     14 1 967 my ($proto, $id) = @_;
13 14   66     43 my $class = ref($proto) || $proto;
14 14         37 my $self = bless { }, $class;
15              
16 14         30 $self->_extract_and_validate($id);
17              
18 14         46 return $self;
19             }
20              
21              
22              
23             sub is_valid
24             {
25 16     16 1 601 my $self = shift;
26              
27 16         70 return $self->{valid};
28             }
29              
30              
31             sub as_string
32             {
33 2     2 1 9 my $self = shift;
34            
35 2 100       5 return unless $self->is_valid();
36              
37 1         9 return "$self->{prefix}$self->{digits}($self->{checksum})";
38             }
39              
40              
41             sub as_string_no_checksum
42             {
43 2     2 1 3 my $self = shift;
44            
45 2 100       6 return unless $self->is_valid();
46              
47 1         5 return "$self->{prefix}$self->{digits}";
48             }
49              
50              
51             # Private methods
52              
53             sub _extract_and_validate
54             {
55 14     14   22 my $self = shift;
56 14         17 my ($raw_id) = @_;
57              
58 14 100       86 return unless defined $raw_id;
59              
60 13         33 $self->{raw_id} = $raw_id;
61              
62 13   100     26 $self->{valid} = $self->_extract_hkid() && $self->_validate_checksum();
63             }
64              
65             sub _extract_hkid
66             {
67 13     13   15 my $self = shift;
68              
69 13 100       118 if ($self->{raw_id} =~ qr
70             {
71             ([a-z]{1,2}) # One or two prefix characters
72             (\d{6}) # Exactly six digits
73             \(* # Optional bracket
74             ([0-9a]) # Checksum, 0-9 or A for 10
75             \)* # Optional bracket
76             }ix)
77             {
78 8         48 ($self->{prefix}, $self->{digits}, $self->{checksum}) =
79             (uc($1), $2, uc($3));
80            
81 8         39 return 1;
82             }
83              
84 5         21 return 0;
85             }
86              
87             sub _validate_checksum
88             {
89 8     8   39 my $self = shift;
90              
91 8         17 return $self->{checksum} eq $self->_calculate_checksum();
92             }
93              
94             sub _calculate_checksum
95             {
96             # Checksum is such that the weighted sum of prefix, digits and checksum
97             # mod 11 is 0. Prefix is converted to a number.
98             # Checksum is encoded as A if value is 10.
99             # eg to find the checksum c in A123456(c)
100             # (1*8 + 1*7 + 2*6 + 3*5 + 4*4 + 5*3 + 6*2 + c*1) % 11 = 0
101             # so c = 3
102              
103 8     8   11 my $self = shift;
104              
105             # Build a list of components from the prefix (converted to
106             # numbers) and the digits
107 8         19 my @components = $self->_prefix_as_numbers();
108 8         30 push @components, split //, $self->{digits};
109              
110             # Sum of weights * components
111 8         12 my $total = 0;
112 8         27 foreach my $weight (reverse(2 .. 1 + scalar @components))
113             {
114 57         110 $total += $weight * shift @components;
115             }
116              
117             # Now solve ($total + $check_digit) % 11 = 0
118 8         20 my $check_digit = (11 - ($total % 11)) % 11;
119 8 100       19 $check_digit = 'A' if $check_digit == 10;
120              
121 8         49 return $check_digit;
122             }
123              
124             sub _prefix_as_numbers
125             {
126             # Convert the prefix characters to a list of numbers
127             # For a two char prefix, A=7, B=8 etc for the first char
128             # For the remaining char or a one char prefix, A=1, B=2 etc
129              
130 8     8   9 my $self = shift;
131              
132 8         25 my @prefix_chars = split //, $self->{prefix};
133 8         17 my @prefix_numbers;
134              
135 8 100       21 if (scalar @prefix_chars == 2)
136             {
137 1         3 push @prefix_numbers, 7 + ord(shift @prefix_chars) - ord('A');
138             }
139              
140 8         17 push @prefix_numbers, 1 + ord(shift @prefix_chars) - ord('A');
141              
142 8         23 return @prefix_numbers;
143             }
144              
145             1;
146              
147             __END__