Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/stdnum/ismn.py
blob: bedc31dfd0cd982d2399692c985b2641c2956946 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# ismn.py - functions for handling ISMNs
#
# Copyright (C) 2010-2017 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""ISMN (International Standard Music Number).

The ISMN (International Standard Music Number) is used to identify sheet
music. This module handles both numbers in the 10-digit 13-digit format.

>>> validate('979-0-3452-4680-5')
'9790345246805'
>>> validate('9790060115615')
'9790060115615'
>>> ismn_type(' M-2306-7118-7')
'ISMN10'
>>> validate('9790060115614')
Traceback (most recent call last):
    ...
InvalidChecksum: ...
>>> compact('  979-0-3452-4680-5')
'9790345246805'
>>> format('9790060115615')
'979-0-060-11561-5'
>>> format('M230671187')
'979-0-2306-7118-7'
>>> to_ismn13('M230671187')
'9790230671187'
"""

from stdnum import ean
from stdnum.exceptions import *
from stdnum.util import clean


def compact(number):
    """Convert the ISMN to the minimal representation. This strips the number
    of any valid ISMN separators and removes surrounding whitespace."""
    return clean(number, ' -.').strip().upper()


def validate(number):
    """Check if the number provided is a valid ISMN (either a legacy 10-digit
    one or a 13-digit one). This checks the length and the check bit but does
    not check if the publisher is known."""
    number = compact(number)
    if len(number) == 10:
        if number[0] != 'M':
            raise InvalidFormat()
        ean.validate('9790' + number[1:])
    elif len(number) == 13:
        if not number.startswith('9790'):
            raise InvalidComponent()
        ean.validate(number)
    else:
        raise InvalidLength()
    return number


def ismn_type(number):
    """Check the type of ISMN number passed and return 'ISMN13', 'ISMN10'
    or None (for invalid)."""
    try:
        number = validate(number)
    except ValidationError:
        return None
    if len(number) == 10:
        return 'ISMN10'
    else:  # len(number) == 13:
        return 'ISMN13'


def is_valid(number):
    """Check if the number provided is a valid ISMN (either a legacy 10-digit
    one or a 13-digit one). This checks the length and the check bit but does
    not check if the publisher is known."""
    try:
        return bool(validate(number))
    except ValidationError:
        return False


def to_ismn13(number):
    """Convert the number to ISMN13 (EAN) format."""
    number = number.strip()
    min_number = compact(number)
    if len(min_number) == 13:
        return number  # nothing to do, already 13 digit format
    # add prefix and strip the M
    if ' ' in number:
        return '979 0' + number[1:]
    elif '-' in number:
        return '979-0' + number[1:]
    else:
        return '9790' + number[1:]


# these are the ranges allocated to publisher codes
_ranges = (
    (3, '000', '099'), (4, '1000', '3999'), (5, '40000', '69999'),
    (6, '700000', '899999'), (7, '9000000', '9999999'))


def split(number):
    """Split the specified ISMN into a bookland prefix (979), an ISMN
    prefix (0), a publisher element (3 to 7 digits), an item element (2 to
    6 digits) and a check digit."""
    # clean up number
    number = to_ismn13(compact(number))
    # find the correct range and split the number
    for length, low, high in _ranges:  # pragma: no branch (all ranges covered)
        if low <= number[4:4 + length] <= high:
            return (number[:3], number[3], number[4:4 + length],
                    number[4 + length:-1], number[-1])


def format(number, separator='-'):
    """Reformat the number to the standard presentation format with the
    prefixes, the publisher element, the item element and the check-digit
    separated by the specified separator. The number is converted to the
    13-digit format silently."""
    return separator.join(x for x in split(number) if x)