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