1from natural.constant import _, IBAN_ALPHABET
2from natural.constant import BBAN_RULES, BBAN_PATTERN, BBAN_MAP
3import re
4
5
6def bban_compact(number):
7    '''
8    Printable compacted Basic Bank Account Number. Removes all the padding
9    characters.
10
11    :param number: string
12
13    >>> bban_compact('1234.56.78.90')
14    '1234567890'
15    >>> bban_compact('068-9999995-01')
16    '068999999501'
17    '''
18    return re.sub(r'[-. ]', '', str(number))
19
20
21def bban_base10(number):
22    '''
23    Printable Basic Bank Account Number in base-10.
24
25    :param number: string
26
27    >>> bban_base10('01234567')
28    '45670123'
29    >>> bban_base10('ABCD')
30    '10111213'
31    '''
32    number = bban_compact(number)
33    number = number[4:] + number[:4]
34    return ''.join([str(IBAN_ALPHABET.index(char)) for char in number])
35
36
37def _bban_regex(structure):
38    return re.compile(
39        r'^%s$' % BBAN_PATTERN.sub(
40            lambda m: '%s{%s}' % (BBAN_MAP[m.group(2)], m.group(1)),
41            structure,
42        )
43    )
44
45
46def bban(value, country=None, validate=False):
47    '''
48    Printable Basic Bank Account Number (BBAN) for the given country code. The
49    ``country`` must be a valid ISO 3166-2 country code.
50
51    :param value: string or int
52    :param country: string
53
54    >>> bban('068-9999995-01', 'BE')
55    '068999999501'
56    >>> bban('555', 'NL')
57    '555'
58    >>> bban('555', 'NL', validate=True)
59    Traceback (most recent call last):
60        ...
61    ValueError: Invalid BBAN, number does not match specification
62    >>> bban('123', 'XY', validate=True)
63    Traceback (most recent call last):
64        ...
65    ValueError: Invalid BBAN, country unknown
66    '''
67
68    value = bban_compact(value)
69
70    if validate:
71        country = country.upper()
72
73        try:
74            rules = BBAN_RULES[country]
75        except KeyError:
76            raise ValueError(_('Invalid BBAN, country unknown'))
77
78        regex = _bban_regex(rules['bban'])
79        if not regex.match(value):
80            raise ValueError(
81                _('Invalid BBAN, number does not match specification')
82            )
83
84    return value
85
86
87def iban(number, validate=False):
88    '''
89    Printable International Bank Account Number (IBAN) as specified in ISO
90    13616.
91
92    :param number: string
93
94    >>> iban('BE43068999999501')
95    'BE43 0689 9999 9501'
96    >>> iban('XY32012341234123', validate=True)
97    Traceback (most recent call last):
98        ...
99    ValueError: Invalid IBAN, country unknown
100    >>> iban('BE43068999999502', validate=True)
101    Traceback (most recent call last):
102        ...
103    ValueError: Invalid IBAN, digits check failed
104
105    '''
106
107    number = bban_compact(number)
108    if validate:
109        country = number[:2]
110        if country not in BBAN_RULES:
111            raise ValueError(_('Invalid IBAN, country unknown'))
112
113        # Do the 10-mod-97 check
114        digits = bban_base10(number)
115        if int(digits) % 97 != 1:
116            raise ValueError(_('Invalid IBAN, digits check failed'))
117
118        # Check BBAN for country
119        bban(number[4:], country, validate=True)
120
121    groups = [number[x:x + 4] for x in range(0, len(number), 4)]
122    return ' '.join(groups)
123