1#######################################################################
2# This package validates ISINs and calculates the check digit
3#######################################################################
4
5package Business::ISIN;
6use Carp;
7require 5.005;
8
9use strict;
10use vars qw($VERSION %country_code);
11$VERSION = '0.20';
12
13use subs qw(check_digit);
14use overload '""' => \&get; # "$isin" shows value
15
16
17# Get list of valid two-letter country codes.
18use Locale::Country;
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
28sub new {
29    my $proto = shift;
30    my $initializer = shift;
31
32    my $class = ref($proto) || $proto;
33    my $self = {value => undef, error => undef};
34    bless ($self, $class);
35
36    $self->set($initializer) if defined $initializer;
37    return $self;
38}
39
40#######################################################################
41# Object Methods
42#######################################################################
43
44sub set {
45    my ($self, $isin) = @_;
46    $self->{value} = $isin;
47    return $self;
48}
49
50sub get {
51    my $self = shift;
52    return undef unless $self->is_valid;
53    return $self->{value};
54}
55
56sub is_valid { # checks if self is a valid ISIN
57    my $self = shift;
58
59    # return not defined $self->error; # or for speed, do this instead
60    return (
61        $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
67sub error {
68    # returns the error string resulting from failure of is_valid
69    my $self = shift;
70    local $_ = $self->{value};
71
72    /^([A-Za-z]{2})? ([A-Za-z0-9]{9})? ([0-9])? (.*)?$/x;
73
74    return "'$_' does not start with a 2-letter country code"
75        unless length $1 > 0 and exists $country_code{uc $1};
76
77    return "'$_' does not have characters 3-11 in [A-Za-z0-9]"
78        unless length $2 > 0;
79
80    return "'$_' character 12 should be a digit"
81        unless length $3 > 0;
82
83    return "'$_' has too many characters"
84        unless length $4 == 0;
85
86    return "'$_' has an inconsistent check digit"
87    	unless $3 == check_digit($1.$2);
88
89    return undef;
90}
91
92
93#######################################################################
94# Subroutines
95#######################################################################
96
97sub check_digit {
98    # takes a 9 digit string, returns the "double-add-double" check digit
99    my $data = uc shift;
100
101    $data =~ /^[A-Z]{2}[A-Z0-9]{9}$/ or croak "Invalid data: $data";
102
103    $data =~ s/([A-Z])/ord($1) - 55/ge; # A->10, ..., Z->35.
104
105    my @n = split //, $data; # take individual digits
106
107    my $max = scalar @n - 1;
108    for my $i (0 .. $max) { if ($i % 2 == 0) { $n[$max - $i] *= 2 } }
109    # double every second digit, starting from the RIGHT hand side.
110
111    for my $i (@n) { $i = $i % 10 + int $i / 10 } # add digits if >=10
112
113    my $sum = 0; for my $i (@n) { $sum += $i } # get the sum of the digits
114
115    return (10 - $sum) % 10; # tens complement, number between 0 and 9
116}
117
1181;
119
120
121
122__END__
123
124=head1 NAME
125
126Business::ISIN - validate International Securities Identification Numbers
127
128=head1 VERSION
129
1300.20
131
132=head1 SYNOPSIS
133
134    use Business::ISIN;
135
136    my $isin = new Business::ISIN 'US459056DG91';
137
138    if ( $isin->is_valid ) {
139	print "$isin is valid!\n";
140	# or: print $isin->get() . " is valid!\n";
141    } else {
142	print "Invalid ISIN: " . $isin->error() . "\n";
143	print "The check digit I was expecting is ";
144	print Business::ISIN::check_digit('US459056DG9') . "\n";
145    }
146
147=head1 REQUIRES
148
149Perl5, Locale::Country, Carp
150
151=head1 DESCRIPTION
152
153C<Business::ISIN> is a class which validates ISINs (International Securities
154Identification Numbers), the codes which identify shares in much the same
155way as ISBNs identify books.  An ISIN consists of two letters, identifying
156the country of origin of the security according to ISO 3166, followed by
157nine characters in [A-Z0-9], followed by a decimal check digit.
158
159The C<new()> method constructs a new ISIN object.  If you give it a scalar
160argument, it will use the argument to initialize the object's value.  Here,
161no attempt will be made to check that the argument is valid.
162
163The C<set()> method sets the ISIN's value to a scalar argument which you
164give.  Here, no attempt will be made to check that the argument is valid.
165The method returns the object, to allow you to do things like
166C<$isin-E<gt>set("GB0004005475")-E<gt>is_valid>.
167
168The C<get()> method returns a string, which will be the ISIN's value if it
169is syntactically valid, and undef otherwise.  Interpolating the object
170reference in double quotes has the same effect (see the synopsis).
171
172The C<is_valid()> method returns true if the object contains a syntactically
173valid ISIN.  (Note: this does B<not> guarantee that a security actually
174exists which has that ISIN.) It will return false otherwise.
175
176If an object does contain an invalid ISIN, then the C<error()> method will
177return a string explaining what is wrong, like any of the following:
178
179=over 4
180
181=item * 'xxx' does not start with a 2-letter country code
182
183=item * 'xxx' does not have characters 3-11 in [A-Za-z0-9]
184
185=item * 'xxx' character 12 should be a digit
186
187=item * 'xxx' has too many characters
188
189=item * 'xxx' has an inconsistent check digit
190
191=back
192
193Otherwise, C<error()> will return C<undef>.
194
195C<check_digit()> is an ordinary subroutine and B<not> a class method.  It
196takes a string of the first eleven characters of an ISIN as an argument (e.g.
197"US459056DG9"), and returns the corresponding check digit, calculated using
198the so-called 'double-add-double' algorithm.
199
200=head1 DIAGNOSTICS
201
202C<check_digit()> will croak with the message 'Invalid data' if you pass it
203an unsuitable argument.
204
205=head1 ACKNOWLEDGEMENTS
206
207Thanks to Peter Dintelmann (Peter.Dintelmann@Dresdner-Bank.com) and Tim
208Ayers (tim.ayers@reuters.com) for suggestions and help debugging this
209module.
210
211=head1 AUTHOR
212
213David Chan <david@sheetmusic.org.uk>
214
215=head1 COPYRIGHT
216
217Copyright (C) 2002, David Chan. All rights reserved. This program is free
218software; you can redistribute it and/or modify it under the same terms as
219Perl itself.
220