Skip to content

Commit d84e909

Browse files
authored
Merge pull request #26 from dhondta/copilot/add-luhn-encoding
Add Luhn / Luhn Mod N checksum codec
2 parents 4c1c316 + d3f47c8 commit d84e909

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

src/codext/checksums/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: UTF-8 -*-
22
from .adler import *
33
from .crc import *
4+
from .luhn import *
45

src/codext/checksums/luhn.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Luhn Codec - Luhn Mod N checksum algorithm.
3+
4+
The Luhn algorithm, also known as the "modulus 10" algorithm, is a simple checksum
5+
formula used to validate identification numbers (e.g. credit card numbers, IMEI
6+
numbers). Encoding appends a check character; decoding verifies the check character
7+
and strips it.
8+
9+
The Luhn Mod N generalization extends the algorithm to alphabets of arbitrary size N.
10+
When called as 'luhn' or 'luhn-10', the standard decimal alphabet (0-9, N=10) is
11+
used. When called as 'luhn-<N>' for 2 ≤ N ≤ 36, the first N characters of
12+
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' form the alphabet.
13+
14+
This codec:
15+
- en/decodes strings from str to str
16+
- en/decodes strings from bytes to bytes
17+
- decodes file content to str (read)
18+
- encodes file content from str to bytes (write)
19+
20+
Reference: https://en.wikipedia.org/wiki/Luhn_algorithm
21+
https://bitcoinwiki.org/wiki/luhn-mod-n-algorithm
22+
"""
23+
from ..__common__ import *
24+
25+
26+
__examples__ = {
27+
'enc(luhn|luhn-10|luhn10)': {
28+
'7992739871': '79927398713',
29+
'': '',
30+
'0': '00',
31+
'1': '18',
32+
},
33+
'dec(luhn|luhn-10|luhn10)': {
34+
'79927398713': '7992739871',
35+
'': '',
36+
'00': '0',
37+
'18': '1',
38+
},
39+
'enc-dec(luhn)': ['123456789', '0' * 10, '9999999999999999'],
40+
'enc-dec(luhn-16)': ['0123456789ABCDEF', 'DEADBEEF'],
41+
'enc-dec(luhn-36)': ['HELLO', 'WORLD123'],
42+
}
43+
44+
_FULL_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
45+
46+
47+
def _luhn_encode(n=""):
48+
mod = n if isinstance(n, int) else 10
49+
alphabet = _FULL_ALPHABET[:mod]
50+
51+
def _encode(text, errors="strict"):
52+
text = ensure_str(text).upper() if mod > 10 else ensure_str(text)
53+
if not text:
54+
return "", 0
55+
for pos, c in enumerate(text):
56+
if c not in alphabet:
57+
handle_error("luhn", errors, kind="character")(c, pos, text)
58+
total = 0
59+
for i, c in enumerate(reversed(text)):
60+
code = alphabet.index(c)
61+
if i % 2 == 0:
62+
d = code * 2
63+
code = d % mod + d // mod
64+
total += code
65+
check = (mod - total % mod) % mod
66+
return text + alphabet[check], len(b(text))
67+
68+
return _encode
69+
70+
71+
def _luhn_decode(n=""):
72+
mod = n if isinstance(n, int) else 10
73+
alphabet = _FULL_ALPHABET[:mod]
74+
75+
def _decode(text, errors="strict"):
76+
text = ensure_str(text).upper() if mod > 10 else ensure_str(text)
77+
if not text:
78+
return "", 0
79+
for pos, c in enumerate(text):
80+
if c not in alphabet:
81+
handle_error("luhn", errors, decode=True, kind="character")(c, pos, text)
82+
total = 0
83+
for i, c in enumerate(reversed(text)):
84+
code = alphabet.index(c)
85+
if i % 2 == 1:
86+
d = code * 2
87+
code = d % mod + d // mod
88+
total += code
89+
if total % mod != 0:
90+
handle_error("luhn", errors, decode=True)(text[-1], len(text) - 1, text[:-1])
91+
return text[:-1], len(b(text))
92+
93+
return _decode
94+
95+
96+
add("luhn", _luhn_encode, _luhn_decode, pattern=r"^luhn[-_]?(\d{1,2})?$", guess=None)

0 commit comments

Comments
 (0)