File Coverage

blib/lib/Business/ISIN.pm
Criterion Covered Total %
statement 56 57 98.2
branch 14 18 77.7
condition 6 9 66.6
subroutine 12 12 100.0
pod 0 5 0.0
total 88 101 87.1


line stmt bran cond sub pod time code
1             #######################################################################
2             # This package validates ISINs and calculates the check digit
3             #######################################################################
4              
5             package Business::ISIN;
6 1     1   670 use Carp;
  1         2  
  1         105  
7             require 5.005;
8              
9 1     1   6 use strict;
  1         2  
  1         38  
10 1     1   4 use vars qw($VERSION %country_code);
  1         13  
  1         74  
11             $VERSION = '0.20';
12              
13 1     1   1104 use subs qw(check_digit);
  1         31  
  1         6  
14 1     1   2212 use overload '""' => \&get; # "$isin" shows value
  1         1242  
  1         10  
15              
16              
17             # Get list of valid two-letter country codes.
18 1     1   1029 use Locale::Country;
  1         47566  
  1         882  
19             $country_code{$_} = 1 for map {uc} Locale::Country::all_country_codes();
20              
21             # Also include the non-country "country codes", used for bonds issued
22             # in multiple countries, etc..
23             $country_code{$_} = 1 for qw(XS XA XB XC XD);
24             #######################################################################
25             # Class Methods
26             #######################################################################
27              
28             sub new {
29 1     1 0 57 my $proto = shift;
30 1         3 my $initializer = shift;
31              
32 1   33     9 my $class = ref($proto) || $proto;
33 1         6 my $self = {value => undef, error => undef};
34 1         3 bless ($self, $class);
35              
36 1 50       4 $self->set($initializer) if defined $initializer;
37 1         4 return $self;
38             }
39              
40             #######################################################################
41             # Object Methods
42             #######################################################################
43              
44             sub set {
45 53     53 0 209 my ($self, $isin) = @_;
46 53         258 $self->{value} = $isin;
47 53         184 return $self;
48             }
49              
50             sub get {
51 2     2 0 4 my $self = shift;
52 2 50       4 return undef unless $self->is_valid;
53 2         11 return $self->{value};
54             }
55              
56             sub is_valid { # checks if self is a valid ISIN
57 47     47 0 68 my $self = shift;
58            
59             # return not defined $self->error; # or for speed, do this instead
60             return (
61 47   66     519 $self->{value} =~ /^(([A-Za-z]{2})([A-Za-z0-9]{9}))([0-9]) $/x
62             and exists $country_code{uc $2}
63             and $4 == check_digit($1)
64             );
65             }
66              
67             sub error {
68             # returns the error string resulting from failure of is_valid
69 6     6 0 8 my $self = shift;
70 6         12 local $_ = $self->{value};
71              
72 6         26 /^([A-Za-z]{2})? ([A-Za-z0-9]{9})? ([0-9])? (.*)?$/x;
73              
74 6 100 100     57 return "'$_' does not start with a 2-letter country code"
75             unless length $1 > 0 and exists $country_code{uc $1};
76              
77 4 100       42 return "'$_' does not have characters 3-11 in [A-Za-z0-9]"
78             unless length $2 > 0;
79              
80 3 100       15 return "'$_' character 12 should be a digit"
81             unless length $3 > 0;
82              
83 2 100       12 return "'$_' has too many characters"
84             unless length $4 == 0;
85              
86 1 50       5 return "'$_' has an inconsistent check digit"
87             unless $3 == check_digit($1.$2);
88              
89 0         0 return undef;
90             }
91              
92              
93             #######################################################################
94             # Subroutines
95             #######################################################################
96              
97             sub check_digit {
98             # takes a 9 digit string, returns the "double-add-double" check digit
99 48     48   98 my $data = uc shift;
100              
101 48 50       151 $data =~ /^[A-Z]{2}[A-Z0-9]{9}$/ or croak "Invalid data: $data";
102              
103 48         165 $data =~ s/([A-Z])/ord($1) - 55/ge; # A->10, ..., Z->35.
  210         546  
104              
105 48         299 my @n = split //, $data; # take individual digits
106              
107 48         103 my $max = scalar @n - 1;
108 48 100       93 for my $i (0 .. $max) { if ($i % 2 == 0) { $n[$max - $i] *= 2 } }
  738         1658  
  389         729  
109             # double every second digit, starting from the RIGHT hand side.
110              
111 48         87 for my $i (@n) { $i = $i % 10 + int $i / 10 } # add digits if >=10
  738         1176  
112              
113 48         68 my $sum = 0; for my $i (@n) { $sum += $i } # get the sum of the digits
  48         67  
  738         848  
114              
115 48         423 return (10 - $sum) % 10; # tens complement, number between 0 and 9
116             }
117              
118             1;
119              
120              
121              
122             __END__