1# figi.py - functions for handling FIGI numbers
2#
3# Copyright (C) 2018 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"""FIGI (Financial Instrument Global Identifier).
21
22The Financial Instrument Global Identifier (FIGI) is a 12-character
23alpha-numerical unique identifier of financial instruments such as common
24stock, options, derivatives, futures, corporate and government bonds,
25municipals, currencies, and mortgage products.
26
27More information:
28
29* https://openfigi.com/
30* https://en.wikipedia.org/wiki/Financial_Instrument_Global_Identifier
31
32>>> validate('BBG000BLNQ16')
33'BBG000BLNQ16'
34>>> validate('BBG000BLNQ14')
35Traceback (most recent call last):
36    ...
37InvalidChecksum: ...
38"""
39
40from stdnum.exceptions import *
41from stdnum.util import clean, isdigits
42
43
44def compact(number):
45    """Convert the number to the minimal representation. This strips the
46    number of any valid separators and removes surrounding whitespace."""
47    return clean(number, ' ').strip().upper()
48
49
50def calc_check_digit(number):
51    """Calculate the check digits for the number."""
52    # we use the full alphabet for the check digit calculation
53    alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
54    # convert to numeric first, then double some, then sum individual digits
55    number = ''.join(
56        str(alphabet.index(n) * (1, 2)[i % 2])
57        for i, n in enumerate(number[:11]))
58    return str((10 - sum(int(n) for n in number)) % 10)
59
60
61def validate(number):
62    """Check if the number provided is a valid FIGI."""
63    number = compact(number)
64    if not all(x in '0123456789BCDFGHJKLMNPQRSTVWXYZ' for x in number):
65        raise InvalidFormat()
66    if len(number) != 12:
67        raise InvalidLength()
68    if isdigits(number[0]) or isdigits(number[1]):
69        raise InvalidFormat()
70    if number[:2] in ('BS', 'BM', 'GG', 'GB', 'VG'):
71        raise InvalidComponent()
72    if number[2] != 'G':
73        raise InvalidComponent()
74    if calc_check_digit(number[:-1]) != number[-1]:
75        raise InvalidChecksum()
76    return number
77
78
79def is_valid(number):
80    """Check if the number provided is a valid FIGI."""
81    try:
82        return bool(validate(number))
83    except ValidationError:
84        return False
85