Add Damm.

This commit is contained in:
Reid 'arrdem' McKenzie 2021-05-15 11:58:11 -06:00
parent 13e6a98d86
commit ea076fb025
4 changed files with 126 additions and 0 deletions

20
projects/damm/BUILD Normal file
View 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
View 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
View 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")

View file

@ -0,0 +1,5 @@
"""
"""
from damm import Damm