File Coverage

blib/lib/Business/CUSIP.pm
Criterion Covered Total %
statement 48 48 100.0
branch 18 18 100.0
condition 5 5 100.0
subroutine 11 11 100.0
pod 8 8 100.0
total 90 90 100.0


line stmt bran cond sub pod time code
1             package Business::CUSIP;
2              
3             =pod
4              
5             =head1 NAME
6              
7             Business::CUSIP - Verify Committee on Uniform Security Identification Procedures Numbers
8              
9             =head1 SYNOPSIS
10              
11             use Business::CUSIP;
12             $csp = Business::CUSIP->new('035231AH2');
13             print "Looks good.\n" if $csp->is_valid;
14              
15             $csp = Business::CUSIP->new('392690QT', 1);
16             $chk = $csp->check_digit;
17             $csp->cusip($csp->cusip.$chk);
18             print $csp->is_valid ? "Looks good." : "Invalid: ", $Business::CUSIP::ERROR, "\n";
19              
20             =head1 DESCRIPTION
21              
22             This module verifies CUSIPs, which are financial identifiers issued by the
23             Standard & Poor's Company for US and Canadian securities. This module cannot
24             tell if a CUSIP references a real security, but it can tell you if the given
25             CUSIP is properly formatted.
26              
27             =cut
28              
29 5     5   45345 use strict;
  5         12  
  5         181  
30 5     5   11125 use Algorithm::LUHN ();
  5         7435  
  5         503  
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 5     5   39 use vars qw($VERSION $ERROR);
  5         18  
  5         3591  
40              
41             $VERSION = '1.03';
42              
43             =head1 METHODS
44              
45             =over 4
46              
47             =item new([CUSIP_NUMBER[, IS_FIXED_INCOME]])
48              
49             The new constructor takes two optional arguments: the CUSIP number and a Boolean
50             value signifying whether this CUSIP refers to a fixed income security. CUSIPs
51             for fixed income securities are validated a little differently than other
52             CUSIPs.
53              
54             =cut
55             sub new {
56 55     55 1 7033 my ($class, $cusip, $fixed_income) = @_;
57 55   100     350 bless [$cusip, ($fixed_income || 0)], $class;
58             }
59              
60             =item cusip([CUSIP_NUMBER])
61              
62             If no argument is given to this method, it will return the current CUSIP
63             number. If an argument is provided, it will set the CUSIP number and then
64             return the CUSIP number.
65              
66             =cut
67             sub cusip {
68 282     282 1 333 my $self = shift;
69 282 100       592 $self->[0] = shift if @_;
70 282         848 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 CUSIP 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 81     81 1 101 my $self = shift;
82 81 100       171 $self->[1] = shift if @_;
83 81         446 return $self->[1];
84             }
85              
86             =item issuer_num()
87              
88             Returns the issuer number from the CUSIP number.
89              
90             =cut
91             sub issuer_num {
92 1     1 1 2 my $self = shift;
93 1         3 return substr($self->cusip, 0, 6);
94             }
95              
96             =item issue_num()
97              
98             Returns the issue number from the CUSIP number.
99              
100             =cut
101             sub issue_num {
102 1     1 1 3 my $self = shift;
103 1         3 return substr($self->cusip, 6, 2);
104             }
105              
106             =item is_valid()
107              
108             Returns true if the checksum of the CUSIP is correct otherwise it returns
109             false and $Business::CUSIP::ERROR will contain a description of the problem.
110              
111             =cut
112             sub is_valid {
113 131     131 1 2999 my $self = shift;
114 131         223 my $val = $self->cusip;
115              
116 131         181 $ERROR = undef;
117              
118             # CUSIPs are 9 digits. Chars 1-3 are numeric. Chars 4-8 are alphanum
119             # plus '@', '#', '*'. Char 9 is numeric.
120 131 100       289 unless (length($val) == 9) {
121 12         16 $ERROR = "CUSIP must be 9 characters long.";
122 12         33 return '';
123             }
124 119 100       392 unless ($val =~ /^\d{3}/) {
125 14         16 $ERROR = "Characters 1-3 must be numeric.";
126 14         34 return '';
127             }
128 105 100       296 unless ($val =~ /^.{3}[A-Z0-9@#*]{5}/) {
129 18         602 $ERROR = "Characters 4-8 must be A-Z, 0-9, @, #, *.";
130 18         40 return '';
131             }
132 87 100       249 unless ($val =~ /\d$/) {
133 12         15 $ERROR = "Character 9 (the check digit) must be numeric.";
134 12         31 return '';
135             }
136              
137             # From the CUSIP spec:
138             # To avoid confusion, the fixed income issue number assignments have
139             # omitted the alphabetic "I" and numeric "1 " as well as the alphabetic
140             # ''O'' and numeric zero.
141             # The issuer number is in positions 7 & 8.
142 75 100 100     144 if ($self->is_fixed_income && substr($self->cusip,6,2) =~ /[I1O0]/) {
143 15         16 $ERROR="Fixed income CUSIP cannot contain I, 1, O, or 0 in the issue number.";
144 15         37 return '';
145             }
146              
147 60         123 my $r = Algorithm::LUHN::is_valid($self->cusip);
148 60 100       3883 $ERROR = $Algorithm::LUHN::ERROR unless $r;
149 60         185 return $r;
150             }
151              
152             =item error()
153              
154             If the CUSIP object is not valid (! is_valid()) it returns the reason it is
155             not valid. Otherwise returns undef.
156              
157             =cut
158             sub error {
159 58     58 1 113 shift->is_valid;
160 58         258 return $ERROR;
161             }
162              
163             =item check_digit()
164              
165             This method returns the checksum of the given object. If the CUSIP number of
166             the object contains a check_digit, it is ignored. In other words this method
167             recalculates the check_digit each time.
168              
169             =cut
170             sub check_digit {
171 28     28 1 101 my $self = shift;
172 28         59 my $r = Algorithm::LUHN::check_digit(substr($self->cusip(), 0, 8));
173 28 100       1624 $ERROR = $Algorithm::LUHN::ERROR unless defined $r;
174 28         68 return $r;
175             }
176              
177             1;
178             __END__