File Coverage

blib/lib/Business/CINS.pm
Criterion Covered Total %
statement 54 54 100.0
branch 20 22 90.9
condition 5 8 62.5
subroutine 13 13 100.0
pod 9 10 90.0
total 101 107 94.3


line stmt bran cond sub pod time code
1             package Business::CINS;
2              
3             =pod
4              
5             =head1 NAME
6              
7             Business::CINS - Verify CUSIP International Numbering System Numbers
8              
9             =head1 SYNOPSIS
10              
11             use Business::CINS;
12             $cn = Business::CINS->new('035231AH2');
13             print "Looks good.\n" if $cn->is_valid;
14              
15             $cn = Business::CINS->new('392690QT', 1);
16             $chk = $cn->check_digit;
17             $cn->cins($cn->cins.$chk);
18             print $cn->is_valid ? "Looks good." : "Invalid: ", $cn->error, "\n";
19              
20             =head1 DESCRIPTION
21              
22             This module verifies CINSes, which are financial identifiers issued by the
23             Standard & Poor's Company for US and Canadian securities. This module cannot
24             tell if a CINS references a real security, but it can tell you if the given
25             CINS is properly formatted.
26              
27             =cut
28              
29 6     6   43407 use strict;
  6         14  
  6         228  
30 6     6   5873 use Algorithm::LUHN ();
  6         5321  
  6         547  
31             # Add additional characters to Algorithm::LUHN::valid_chars so CUSIPs can be
32             # validated.
33             {
34             my $ct = 10;
35             Algorithm::LUHN::valid_chars(map {$_ => $ct++} 'A'..'Z');
36             }
37             #Algorithm::LUHN::valid_chars('*',36, '@',37, '#',38);
38              
39 6     6   38 use vars qw($VERSION $ERROR);
  6         19  
  6         5365  
40              
41             $VERSION = '1.13';
42              
43             =head1 METHODS
44              
45             =over 4
46              
47             =item new([CINS_NUMBER[, IS_FIXED_INCOME]])
48              
49             The new constructor takes two optional arguments: the CINS number and a Boolean
50             value signifying whether this CINS refers to a fixed income security. CINSes
51             for fixed income securities are validated a little differently than other
52             CINSes.
53              
54             =cut
55             sub new {
56 46     46 1 8794 my ($class, $cins, $fixed_income) = @_;
57 46   100     341 bless [$cins, ($fixed_income || 0)], $class;
58             }
59              
60             =item cins([CINS_NUMBER])
61              
62             If no argument is given to this method, it will return the current CINS
63             number. If an argument is provided, it will set the CINS number and then
64             return the CINS number.
65              
66             =cut
67             sub cins {
68 328     328 1 387 my $self = shift;
69 328 100       664 $self->[0] = shift if @_;
70 328         1097 return $self->[0];
71             }
72              
73             =item is_fixed_income([TRUE_OR_FALSE])
74              
75             If no argument is given to this method, it will return whether the CINS object
76             is marked as a fixed income security. If an argument is provided, it will set
77             the fixed income property and then return the fixed income setting.
78              
79             =cut
80             sub is_fixed_income {
81 65     65 1 96 my $self = shift;
82 65 100       144 $self->[1] = shift if @_;
83 65         175 return $self->[1];
84             }
85              
86             =item domicile_code ()
87              
88             Returns the domicile code from the CINS number.
89              
90             =cut
91             sub domicile_code {
92 101     101 1 117 my $self = shift;
93 101         170 return substr($self->cins, 0, 1);
94             }
95              
96             =item issuer_num()
97              
98             Returns the issuer number from the CINS number.
99              
100             =cut
101             sub issuer_num {
102 1     1 1 2 my $self = shift;
103 1         3 return substr($self->cins, 1, 5);
104             }
105              
106             =item issuer_num()
107              
108             Returns the issue number from the CINS number.
109              
110             =cut
111             sub issue_num {
112 29     29 0 33 my $self = shift;
113 29         54 return substr($self->cins, 6, 2);
114             }
115              
116             =item is_valid()
117              
118             Returns true if the checksum of the CINS is correct otherwise it returns
119             false and $Business::CINS::ERROR will contain a description of the problem.
120              
121             =cut
122             sub is_valid {
123 112     112 1 4028 my $self = shift;
124 112         210 my $val = $self->cins;
125              
126 112         152 $ERROR = undef;
127              
128             # The CINS number consists of nine characters. The first six (6) positions,
129             # known as the issuer number, consist of a country or regional alpha code of
130             # one character, plus five positions, the last of which may be alpha or
131             # numeric. A two character suffix (either numeric or alphabetic or both)
132             # known as the issue number follows. The ninth character is a check digit.
133              
134 112 100       436 unless (length($val) == 9) {
135 12         16 $ERROR = "CINS must be 9 characters long.";
136 12         33 return '';
137             }
138 100 100       195 unless (Business::CINS->domicile_descr($self->domicile_code)) {
139 9         10 $ERROR = "First character is not a valid domicile code.";
140 9         28 return '';
141             }
142             # unless ($val =~ /^.\d{4}/) {
143             # $ERROR = "Characters 2-5 must be numeric.";
144             # return '';
145             # }
146 91 100       489 unless ($val =~ /^.[A-Z0-9]{7}/) {
147 17         21 $ERROR = "Characters 2-8 must be A-Z, 0-9.";
148 17         44 return '';
149             }
150 74 100       231 unless ($val =~ /\d$/) {
151 15         19 $ERROR = "Character 9 (the check digit) must be numeric.";
152 15         55 return '';
153             }
154              
155             # From the CINS spec:
156              
157             # Issue Numbers for Fixed Income Securities: The issue number assigned to an
158             # issuer's fixed income securities may consist of two alphabetic characters
159             # (AA, etc.), one alphabetic character followed by one digit (A2, etc.) or
160             # one digit followed by one alphabetic character (2A, etc.), assigned in that
161             # order. Debt securities will be sorted in order by their maturity dates.
162 59 100       149 if ($self->is_fixed_income) {
163 28         132 my $issue_num = $self->issue_num;
164 28 50 66     261 if ($issue_num !~ /^[A-Z][A-Z]$/ and
      33        
165             $issue_num !~ /^[A-Z][0-9]$/ and
166             $issue_num !~ /^[0-9][A-Z]$/) {
167 12         18 $ERROR = "Fixed income issue number must be alpha-alpha, alpha-num or num-alpha only.";
168 12         43 return '';
169             }
170             }
171              
172 47         96 my $r = Algorithm::LUHN::is_valid($self->cins);
173 47 100       3487 $ERROR = $Algorithm::LUHN::ERROR unless $r;
174 47         155 return $r;
175             }
176              
177             =item error()
178              
179             If the CINS object is not valid (! is_valid()) it returns the reason it is
180             Not valid. Otherwise returns undef.
181              
182             =cut
183             sub error {
184 56     56 1 116 shift->is_valid;
185 56         271 return $ERROR;
186             }
187              
188             =item check_digit()
189              
190             This method returns the checksum of the given object. If the CINS number of
191             the object contains a check_digit, it is ignored. In other words this method
192             recalculates the check_digit each time.
193              
194             =cut
195             sub check_digit {
196 20     20 1 77 my $self = shift;
197 20         51 my $r = Algorithm::LUHN::check_digit(substr($self->cins(), 0, 8));
198 20 100       1132 $ERROR = $Algorithm::LUHN::ERROR unless defined $r;
199 20         54 return $r;
200             }
201              
202             =item Business::CINS->domicile_descr([CODE])
203              
204             Given a domicile code it will return a description of the code. The valid
205             domicile codes are
206              
207             A = Austria J = Japan S = South Africa
208             B = Belgium K = Denmark T = Italy
209             C = Canada L = Luxembourg U = United States
210             D = Germany M = Mid-East V = Africa - Other
211             E = Spain N = Netherlands W = Sweden
212             F = France P = South America X = Europe-Other
213             G = United Kingdom Q = Australia Y = Asia
214             H = Switzerland R = Norway
215              
216             If no CODE is given, it will return the hash of codes.
217              
218             =cut
219             {
220             my %domicile_cds =
221             (A => 'Austria', J => 'Japan', S => 'South Africa',
222             B => 'Belgium', K => 'Denmark', T => 'Italy',
223             C => 'Canada', L => 'Luxembourg', U => 'United States',
224             D => 'Germany', M => 'Mid-East', V => 'Africa - Other',
225             E => 'Spain', N => 'Netherlands', W => 'Sweden',
226             F => 'France', P => 'South America', X => 'Europe-Other',
227             G => 'United Kingdom', Q => 'Australia', Y => 'Asia',
228             H => 'Switzerland', R => 'Norway',
229             );
230             sub domicile_descr {
231 126     126 1 10351 shift; # ignore the first argument.
232 126 50       654 return (@_ ? $domicile_cds{$_[0]} : %domicile_cds);
233             }
234             }
235              
236             1;
237             __END__