1# cusip.py - functions for handling CUSIP numbers
2#
3# Copyright (C) 2015-2017 Arthur de Jong
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18# 02110-1301 USA
19
20"""CUSIP number (financial security identification number).
21
22CUSIP (Committee on Uniform Securities Identification Procedures) numbers are
23used to identify financial securities. CUSIP numbers are a nine-character
24alphanumeric code where the first six characters identify the issuer,
25followed by two digits that identify and a check digit.
26
27More information:
28
29* https://en.wikipedia.org/wiki/CUSIP
30* https://www.cusip.com/
31
32>>> validate('DUS0421C5')
33'DUS0421C5'
34>>> validate('DUS0421CN')
35Traceback (most recent call last):
36    ...
37InvalidChecksum: ...
38>>> to_isin('91324PAE2')
39'US91324PAE25'
40"""
41
42from stdnum.exceptions import *
43from stdnum.util import clean
44
45
46def compact(number):
47    """Convert the number to the minimal representation. This strips the
48    number of any valid separators and removes surrounding whitespace."""
49    return clean(number, ' ').strip().upper()
50
51
52# O and I are not valid but are accounted for in the check digit calculation
53_alphabet = '0123456789ABCDEFGH JKLMN PQRSTUVWXYZ*@#'
54
55
56def calc_check_digit(number):
57    """Calculate the check digits for the number."""
58    # convert to numeric first, then sum individual digits
59    number = ''.join(
60        str((1, 2)[i % 2] * _alphabet.index(n)) for i, n in enumerate(number))
61    return str((10 - sum(int(n) for n in number)) % 10)
62
63
64def validate(number):
65    """Check if the number provided is valid. This checks the length and
66    check digit."""
67    number = compact(number)
68    if not all(x in _alphabet for x in number):
69        raise InvalidFormat()
70    if len(number) != 9:
71        raise InvalidLength()
72    if calc_check_digit(number[:-1]) != number[-1]:
73        raise InvalidChecksum()
74    return number
75
76
77def is_valid(number):
78    """Check if the number provided is valid. This checks the length and
79    check digit."""
80    try:
81        return bool(validate(number))
82    except ValidationError:
83        return False
84
85
86def to_isin(number):
87    """Convert the number to an ISIN."""
88    from stdnum import isin
89    return isin.from_natid('US', number)
90