1# damm.py - functions for performing the Damm checksum algorithm
2#
3# Copyright (C) 2016-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"""The Damm algorithm.
21
22The Damm algorithm is a check digit algorithm that should detect all
23single-digit errors and all adjacent transposition errors. Based on
24anti-symmetric quasigroup of order 10 it uses a substitution table.
25
26This implementation uses the table from Wikipedia by default but a custom
27table can be provided.
28
29More information:
30
31* https://en.wikipedia.org/wiki/Damm_algorithm
32
33>>> validate('572')
34Traceback (most recent call last):
35    ...
36InvalidChecksum: ...
37>>> calc_check_digit('572')
38'4'
39>>> validate('5724')
40'5724'
41>>> table = (
42...     (0, 2, 3, 4, 5, 6, 7, 8, 9, 1),
43...     (2, 0, 4, 1, 7, 9, 5, 3, 8, 6),
44...     (3, 7, 0, 5, 2, 8, 1, 6, 4, 9),
45...     (4, 1, 8, 0, 6, 3, 9, 2, 7, 5),
46...     (5, 6, 2, 9, 0, 7, 4, 1, 3, 8),
47...     (6, 9, 7, 3, 1, 0, 8, 5, 2, 4),
48...     (7, 5, 1, 8, 4, 2, 0, 9, 6, 3),
49...     (8, 4, 6, 2, 9, 5, 3, 0, 1, 7),
50...     (9, 8, 5, 7, 3, 1, 6, 4, 0, 2),
51...     (1, 3, 9, 6, 8, 4, 2, 7, 5, 0))
52>>> checksum('816', table=table)
539
54"""
55
56from stdnum.exceptions import *
57
58
59_operation_table = (
60    (0, 3, 1, 7, 5, 9, 8, 6, 4, 2),
61    (7, 0, 9, 2, 1, 5, 4, 8, 6, 3),
62    (4, 2, 0, 6, 8, 7, 1, 3, 5, 9),
63    (1, 7, 5, 0, 9, 8, 3, 4, 2, 6),
64    (6, 1, 2, 3, 0, 4, 5, 9, 7, 8),
65    (3, 6, 7, 4, 2, 0, 9, 5, 8, 1),
66    (5, 8, 6, 9, 7, 2, 0, 1, 3, 4),
67    (8, 9, 4, 5, 3, 6, 2, 0, 1, 7),
68    (9, 4, 3, 8, 6, 1, 7, 2, 0, 5),
69    (2, 5, 8, 1, 4, 3, 6, 7, 9, 0))
70
71
72def checksum(number, table=None):
73    """Calculate the Damm checksum over the provided number. The checksum is
74    returned as an integer value and should be 0 when valid."""
75    table = table or _operation_table
76    i = 0
77    for n in str(number):
78        i = table[i][int(n)]
79    return i
80
81
82def validate(number, table=None):
83    """Check if the number provided passes the Damm algorithm."""
84    if not bool(number):
85        raise InvalidFormat()
86    try:
87        valid = checksum(number, table=table) == 0
88    except Exception:
89        raise InvalidFormat()
90    if not valid:
91        raise InvalidChecksum()
92    return number
93
94
95def is_valid(number, table=None):
96    """Check if the number provided passes the Damm algorithm."""
97    try:
98        return bool(validate(number, table=table))
99    except ValidationError:
100        return False
101
102
103def calc_check_digit(number, table=None):
104    """Calculate the extra digit that should be appended to the number to
105    make it a valid number."""
106    return str(checksum(number, table=table))
107