Add Damm.
This commit is contained in:
parent
13e6a98d86
commit
ea076fb025
4 changed files with 126 additions and 0 deletions
20
projects/damm/BUILD
Normal file
20
projects/damm/BUILD
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
py_library(
|
||||||
|
name = "damm",
|
||||||
|
srcs = [
|
||||||
|
"damm.py",
|
||||||
|
],
|
||||||
|
imports = [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
py_pytest(
|
||||||
|
name = "test_damm",
|
||||||
|
srcs = glob(["test_*.py"]),
|
||||||
|
deps = [
|
||||||
|
":damm",
|
||||||
|
]
|
||||||
|
)
|
23
projects/damm/README.md
Normal file
23
projects/damm/README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Damm
|
||||||
|
|
||||||
|
An implementation of a Damm check digits.
|
||||||
|
|
||||||
|
doi:10.2174/9781608058822114010013
|
||||||
|
|
||||||
|
To get into the group theory here a bit, Damm showed that it was possible to construct an anti-symmetric quasigroup for most orders (group sizes).
|
||||||
|
A quasigroup is a non-associative group for which division is always well defined.
|
||||||
|
Damm's check digits are based on this non-associative property which allows for the full detection of transposition errors.
|
||||||
|
|
||||||
|
We can use a quasigroup to implement a check digit scheme.
|
||||||
|
Checking a number consists of "multiplying" the digits of the number together through the group's multiplication table.
|
||||||
|
|
||||||
|
The neat bit of Damm's quasigroups is the property `x · x = 0`.
|
||||||
|
This is visible in the multiplication matrix below.
|
||||||
|
What this means is that if the Damm code (product of all digits within the Damm group!)
|
||||||
|
of a numeric string is x, the product of THAT product with itself is 0.
|
||||||
|
Which means we can compute the Damm coding of any number simply by APPENDING the product of its digits.
|
||||||
|
|
||||||
|
Damm checking is then defined by computing the product of the digits within a Damm group and affirming that the product is 0 which is only possible if the input string is '0' OR it is a valid Damm checked number.
|
||||||
|
|
||||||
|
Another nice trick with the Damm quasigroup is that it's safe to PREFIX zeros to any Damm coded number without changing the Damm correction code.
|
||||||
|
Unfortunately this also means that SUFFIXING zeros won't impact the Damm code either.
|
78
projects/damm/damm.py
Normal file
78
projects/damm/damm.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
An implementation of Damm check digits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
|
class DammException(Exception):
|
||||||
|
"""Base class for all exceptions in the module."""
|
||||||
|
|
||||||
|
|
||||||
|
class CheckDigitException(DammException):
|
||||||
|
"""Thrown when a string did not check when decoding a supposedly checked string."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoMatchException(DammException):
|
||||||
|
"""Thrown when a string did not match."""
|
||||||
|
|
||||||
|
|
||||||
|
class Damm(NamedTuple):
|
||||||
|
"""A Damm coded number."""
|
||||||
|
|
||||||
|
MATRIX = (
|
||||||
|
(0, 3, 1, 7, 5, 9, 8, 6, 4, 2),
|
||||||
|
(7, 0, 9, 2, 1, 5, 4, 8, 6, 3),
|
||||||
|
(4, 2, 0, 6, 8, 7, 1, 3, 5, 9),
|
||||||
|
(1, 7, 5, 0, 9, 8, 3, 4, 2, 6),
|
||||||
|
(6, 1, 2, 3, 0, 4, 5, 9, 7, 8),
|
||||||
|
(3, 6, 7, 4, 2, 0, 9, 5, 8, 1),
|
||||||
|
(5, 8, 6, 9, 7, 2, 0, 1, 3, 4),
|
||||||
|
(8, 9, 4, 5, 3, 6, 2, 0, 1, 7),
|
||||||
|
(9, 4, 3, 8, 6, 1, 7, 2, 0, 5),
|
||||||
|
(2, 5, 8, 1, 4, 3, 6, 7, 9, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
DAMM_PATTERN = re.compile(r"(?P<value>\d+)-?(?P<check>\d)")
|
||||||
|
|
||||||
|
value: int
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.value}-{self.encode(self.value)}"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.value}, {self.encode(self.value)})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def encode(cls, value):
|
||||||
|
state = 0
|
||||||
|
for digit in str(value):
|
||||||
|
state = cls.MATRIX[state][int(digit)]
|
||||||
|
return state
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify(cls, value: str) -> bool:
|
||||||
|
"""Verify a Damm encoded number, returning its value if valid.
|
||||||
|
|
||||||
|
The core Damm encoding property is that the LAST digit is ALWAYS the check. When the rest of
|
||||||
|
the string is correctly Damm encoded, the Damm of the entire string will be 0.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return cls.encode(int(value.replace("-", ""))) == 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_str(cls, value: str) -> "Damm":
|
||||||
|
"""Verify a Damm coded string, and return its decoding."""
|
||||||
|
if match := re.fullmatch(cls.DAMM_PATTERN, value):
|
||||||
|
given_value = match.group("value")
|
||||||
|
computed_code = cls.encode(given_value)
|
||||||
|
given_code = int(match.group("check"))
|
||||||
|
if computed_code == given_code:
|
||||||
|
return Damm(int(given_value))
|
||||||
|
else:
|
||||||
|
raise CheckDigitException(
|
||||||
|
f"Value {value!r} had check digit {given_code!r}, but computed check was {computed_code!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NoMatchException(f"Input {value!r} was not a valid decimal number")
|
5
projects/damm/test_damm.py
Normal file
5
projects/damm/test_damm.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from damm import Damm
|
Loading…
Reference in a new issue