1import hashlib
2import re
3import six
4from binascii import unhexlify
5from natural.constant import PHONE_PREFIX, PHONE_E161_ALPHABET
6from natural.language import _
7from natural.util import luhn_append, luhn_calc, strip, to_decimal
8
9
10def e123(number, areasize=3, groupsize=4, national=False):
11    '''
12    Printable E.123 (Notation for national and international telephone numbers
13    from ITU) numbers.
14
15    :param number: string
16    :param areasize: int
17    :param groupsize: int
18    :param national: bool
19
20    >>> e123(155542315678)
21    '+1 555 4231 5678'
22    >>> e123('+31654231567', areasize=1)
23    '+31 6 5423 1567'
24    >>> e123('+3114020', areasize=2)
25    '+31 14 020'
26    >>> e123('+312054231567', areasize=2, national=True)
27    '(020) 5423 1567'
28    '''
29
30    if isinstance(number, six.integer_types):
31        return e123('+%s' % number, areasize, groupsize)
32
33    elif isinstance(number, six.string_types):
34        number = strip(number, '-. ()')
35        if number.startswith('+'):
36            number = number[1:]
37
38        if not number.isdigit():
39            raise ValueError(_('Invalid telephone number'))
40
41        groups = []
42        prefix = ''
43        remain = number
44
45        if national:
46            for x in six.moves.xrange(3, 0, -1):
47                if number[:x] in PHONE_PREFIX:
48                    groups.append('(0%s)' % number[x:x + areasize])
49                    remain = number[x + areasize:]
50                    break
51
52        else:
53            prefix = '+'
54            for x in six.moves.xrange(3, 0, -1):
55                if number[:x] in PHONE_PREFIX:
56                    groups.append(number[:x])
57                    groups.append(number[x:x + areasize])
58                    remain = number[x + areasize:]
59                    break
60
61        for x in six.moves.xrange(0, len(remain) + 1, groupsize):
62            groups.append(remain[x:x + groupsize])
63
64        return '%s%s' % (prefix, ' '.join(list(filter(None, groups))))
65
66
67def e161(number, alphabet=PHONE_E161_ALPHABET):
68    '''
69    Printable a 26 Latin letters (A to Z) phone number to the 12-key telephone
70    keypad number.
71
72    :param number: string
73    :param alphabet: dict
74
75    >>> e161('0800-PIZZA123')
76    '080074992123'
77    >>> e161('0800^PIZZA123')
78    Traceback (most recent call last):
79        ...
80    ValueError: Character "^" (0x5e) is not in the E.161 alphabet
81    '''
82
83    digits = []
84    for char in strip(number, '+-. ()').lower():
85        length = len(digits)
86        for group, digit in alphabet.items():
87            if char in group:
88                digits.append(digit)
89                break
90
91        if len(digits) == length:
92            raise ValueError(
93                _('Character "%s" (0x%02x) is not in the E.161 alphabet') %
94                (char, ord(char))
95            )
96
97    return u''.join(digits)
98
99
100def e164(number):
101    '''
102    Printable E.164 (The international public telecommunication numbering plan
103    from ITU) numbers.
104
105    :param number: string
106
107    >>> e164(155542315678)
108    '+155542315678'
109    >>> e164('+31 20 5423 1567')
110    '+312054231567'
111    '''
112    if isinstance(number, six.integer_types):
113        return e164('+%s' % number)
114
115    elif isinstance(number, six.string_types):
116        number = strip(number, '-. ()')
117        if number.startswith('+'):
118            number = number[1:]
119
120        return u'+%s' % number
121
122
123def enum(number, zone='e164.arpa'):
124    '''
125    Printable DNS ENUM (telephone number mapping) record.
126
127    :param number: string
128    :param zone: string
129
130
131    >>> enum('+31 20 5423 1567')
132    '7.6.5.1.3.2.4.5.0.2.1.3.e164.arpa.'
133    >>> enum('+31 97 99 6642', zone='e164.spacephone.org')
134    '2.4.6.6.9.9.7.9.1.3.e164.spacephone.org.'
135
136    '''
137    number = e164(number).lstrip('+')
138    return u'.'.join([
139        u'.'.join(number[::-1]),
140        zone.strip(u'.'),
141        '',
142    ])
143
144
145def imei(number):
146    '''
147    Printable International Mobile Station Equipment Identity (IMEI) numbers.
148
149    :param number: string or int
150
151    >>> imei(12345678901234)
152    '12-345678-901234-7'
153    >>> imei(1234567890123456)
154    '12-345678-901234-56'
155    '''
156    number = to_decimal(number)
157    length = len(number)
158    if length not in (14, 15, 16):
159        raise ValueError(
160            _('Invaid International Mobile Station Equipment Identity')
161        )
162
163    if len(number) == 14:
164        # Add Luhn check digit
165        number = luhn_append(number)
166
167    groups = (number[:2], number[2:8], number[8:14], number[14:])
168    return u'-'.join(list(filter(None, groups)))
169
170
171def imsi(number):
172    '''
173    Printable International Mobile Subscriber Identity (IMSI) numbers. Mind
174    that there is no validation done on the actual correctness of the MCC/MNC.
175    If you wish to validate IMSI numbers, take a look at `python-stdnum`_.
176
177    :param number: string or int
178
179    >>> imsi(2042312345)
180    '204-23-12345'
181
182    .. _python-stdnum: https://pypi.python.org/pypi/python-stdnum/
183    '''
184    number = to_decimal(number)
185    groups = (number[:3], number[3:5], number[5:])
186    return u'-'.join(list(filter(None, groups)))
187
188
189def meid(number, separator=u' '):
190    '''
191    Printable Mobile Equipment Identifier (MEID) number.
192
193    >>> meid(123456789012345678)
194    '1B 69B4BA 630F34 6'
195    >>> meid('1B69B4BA630F34')
196    '1B 69B4BA 630F34 6'
197    '''
198
199    if isinstance(number, six.string_types):
200        number = re.sub(r'[\s-]', '', number)
201
202        try:
203            number = '%014X' % int(number, 16)
204        except ValueError:
205            if len(number) < 18 and number.isdigit():
206                return meid('%014X' % int(number), separator)
207            else:
208                raise ValueError(_('Invalid MEID, size mismatch'))
209        else:
210            if len(number) not in (14, 15):
211                raise ValueError(_('Invalid MEID, size mismatch'))
212
213    elif isinstance(number, six.integer_types):
214        if number > 0xfffffffffffffff:
215                raise ValueError(_('Invalid MEID, size mismatch'))
216        return meid(('%014X' % number)[:14], separator)
217
218    else:
219        raise TypeError(_('Invalid MEID, input type invalid'))
220
221    number = number.upper()
222    region = number[:2]
223    manufacturer = number[2:8]
224    serial_number = number[8:14]
225    check_digit = number[14:]
226
227    if check_digit == '':
228        check_digit = luhn_calc(number, chars='0123456789ABCDEF')
229
230    groups = (region, manufacturer, serial_number, check_digit)
231    return separator.join(list(filter(None, groups)))
232
233
234def pesn(number, separator=u''):
235    '''
236    Printable Pseudo Electronic Serial Number.
237
238    :param number: hexadecimal string
239
240    >>> pesn('1B69B4BA630F34E')
241    '805F9EF7'
242    '''
243
244    number = re.sub(r'[\s-]', '', meid(number))
245    serial = hashlib.sha1(unhexlify(number[:14]))
246    return separator.join(['80', serial.hexdigest()[-6:].upper()])
247