Table of Contents

Check digits

Check-digit algorithms validate human-readable identifiers - credit card numbers, barcodes, bank account numbers, securities codes - by appending a short computed suffix that lets any reader confirm the identifier was not mis-typed or mis-transcribed. They target the specific error patterns that humans introduce when copying a string by hand: single-digit substitutions, adjacent transpositions, twin errors (e.g. 11 → 22), and jump transpositions.

Note. Check-digit algorithms are not cryptographic and must not be used for password hashing, digital signatures, or integrity validation in security-sensitive applications. They are error-detection primitives for human-readable identifiers, nothing more.

One namespace, one root, three base classes

Every check-digit type in this package lives in the single Bodu.IO.Hashing.CheckDigits namespace. Every one of them ultimately derives from the root CheckValueAlgorithm (AlgorithmName, CheckLength, Append, GetCurrentCheckValue, Reset). What differs between them is which of its three abstract derivatives each one extends - and that base captures the input alphabet and the output shape:

Base class Input alphabet Output Types
CheckDigitAlgorithm ASCII decimal digits ('0'-'9') A single char Luhn, Damm, Verhoeff, Ean8, Ean13, Gtin14, UpcA, Isbn13, AbaRoutingNumber
AlphanumericCheckDigitAlgorithm Digits and/or letters A single char (may be 'X') Isin, Isbn10, Sedol, Cusip, Iso7064Mod11_2
MultiCharCheckDigitAlgorithm Digits and/or letters A fixed-length string (typically two digits) Iban, Lei, Iso7064Mod97_10

All three bases expose the same streaming idiom: Append digits or characters into the running state; call GetCurrentCheckDigit() (single-char bases) or GetCurrentCheckDigits() (the multi-char base) - or the root's GetCurrentCheckValue() string form on any of them - to read the result non-destructively; call Reset() to restart. The static Compute / IsValid helpers wrap that lifecycle for the common one-shot case.

Note

Every example below uses using Bodu.IO.Hashing.CheckDigits; - there is no longer a separate Checksums namespace for the alphanumeric or multi-character schemes.


Decimal check-digit algorithms - Bodu.IO.Hashing.CheckDigits

Luhn - Luhn

The Luhn algorithm (ISO/IEC 7812, also called modulus 10 or mod 10) was designed by Hans Peter Luhn at IBM in 1954. It is the check-digit scheme used by virtually every payment card number (Visa, Mastercard, Amex, Discover), IMEI numbers, and many national identification numbers.

Error detection: catches all single-digit substitution errors. Catches all adjacent transpositions except the 09 ↔ 90 swap.

using Bodu.IO.Hashing.CheckDigits;

// Streaming - append the body digits and read the check digit.
var luhn = new Luhn();
luhn.Append("7992739871");
char check = luhn.GetCurrentCheckDigit();   // '3'  →  full number "79927398713"

// One-shot static helpers.
char computed = Luhn.Compute("7992739871");          // '3'
bool valid    = Luhn.IsValid("79927398713");         // true
bool invalid  = Luhn.IsValid("79927398710");         // false

Damm - Damm

The Damm algorithm uses a quasigroup operation table designed by H. Michael Damm (2004). It detects all single-digit substitution errors and all adjacent transposition errors - including the 09 ↔ 90 swap that Luhn misses. It also detects many twin errors.

using Bodu.IO.Hashing.CheckDigits;

var damm = new Damm();
damm.Append("572");
char check = damm.GetCurrentCheckDigit();   // '4'  →  "5724" is valid

bool valid = Damm.IsValid("5724");          // true

Verhoeff - Verhoeff

The Verhoeff algorithm uses the dihedral group D₅ and a permutation table to detect all single-digit substitution errors, all adjacent transpositions, and all twin errors. It was designed by Jacobus Verhoeff (1969) and is used by the German ID card system and various medical device identifiers.

using Bodu.IO.Hashing.CheckDigits;

var verhoeff = new Verhoeff();
verhoeff.Append("236");
char check = verhoeff.GetCurrentCheckDigit();   // '3'  →  "2363" is valid

bool valid = Verhoeff.IsValid("2363");          // true

EAN barcodes - Ean8 / Ean13

EAN-8 and EAN-13 use the GS1 weighted-mod-10 algorithm. They are the standard barcodes on retail products worldwide.

using Bodu.IO.Hashing.CheckDigits;

// EAN-13: 12-digit body → 1 check digit.
var ean13 = new Ean13();
ean13.Append("590123412345");
char check = ean13.GetCurrentCheckDigit();   // '7'  →  "5901234123457"

bool valid = Ean13.IsValid("5901234123457");   // true

// EAN-8: 7-digit body → 1 check digit.
char check8 = Ean8.Compute("1234567");   // '0'

GTIN-14 - Gtin14

GTIN-14 extends EAN-13 with a packaging-level indicator digit, using the same GS1 weighted-mod-10 algorithm. It is the standard for shipping cartons and pallet-level barcodes.

using Bodu.IO.Hashing.CheckDigits;

char check = Gtin14.Compute("1234567890123");   // '1'
bool valid  = Gtin14.IsValid("12345678901231");  // true

UPC-A - UpcA

UPC-A is the standard 12-digit barcode used in the United States and Canada. It is structurally identical to EAN-13 with a leading zero, using the same GS1 weighted-mod-10 algorithm.

using Bodu.IO.Hashing.CheckDigits;

char check = UpcA.Compute("03600024145");   // '7'
bool valid  = UpcA.IsValid("036000241457");  // true

ISIN - Isin

An ISIN (International Securities Identification Number, ISO 6166) is a 12-character alphanumeric code identifying a financial security. The check digit is computed by expanding each letter to two digits (A=10, B=11, …, Z=35), concatenating the result with the numeric body, then applying the Luhn algorithm.

using Bodu.IO.Hashing.CheckDigits;

// 11-character body (2-letter country code + 9-character NSIN).
char check = Isin.Compute("US037833100");   // '5'  →  "US0378331005"

bool valid = Isin.IsValid("US0378331005");   // true  (Apple Inc.)

ABA routing number - AbaRoutingNumber

US bank routing numbers use a weighted-mod-10 scheme with weights [3, 7, 1] repeating. The ABA (American Bankers Association) routing transit number is always 9 digits.

using Bodu.IO.Hashing.CheckDigits;

bool valid = AbaRoutingNumber.IsValid("021000021");   // true (JPMorgan Chase, NY)

Alphanumeric and multi-character algorithms - Bodu.IO.Hashing.CheckDigits

IBAN - Iban

An IBAN (International Bank Account Number, ISO 13616) begins with a two-letter country code followed by two check digits and the country-specific BBAN. The check uses ISO 7064 MOD 97-10 over the rearranged and letter-expanded string.

using Bodu.IO.Hashing.CheckDigits;

// Body = country code + BBAN (without the two check digits).
var iban = new Iban();
iban.Append("GB");       // country code
iban.Append("BARC20201530093459");   // BBAN
Span<char> checkBuf = stackalloc char[2];
iban.GetCurrentCheckDigits(checkBuf);   // "29"  →  "GB29 BARC 2020 1530 0934 59"

bool valid = Iban.IsValid("GB29BARC20201530093459");   // true

ISBN - Isbn10 / Isbn13

Isbn10 uses weighted mod-11 (the check digit may be 'X' representing 10). Isbn13 uses GS1 weighted mod-10 and is identical to EAN-13. Both share the same streaming API.

using Bodu.IO.Hashing.CheckDigits;

char check10 = Isbn10.Compute("030640615");   // '2'  →  "0306406152"
char check13 = Isbn13.Compute("978030640615");   // '2'

bool valid10 = Isbn10.IsValid("0306406152");
bool valid13 = Isbn13.IsValid("9780306406157");

SEDOL - Sedol

SEDOL (Stock Exchange Daily Official List) is a 7-character identifier used by the London Stock Exchange. The 6-character body uses digits and uppercase consonants (vowels are excluded); the check digit is the result of a weighted mod-10 computation.

using Bodu.IO.Hashing.CheckDigits;

char check = Sedol.Compute("710889");   // '2'  →  "7108892"
bool valid  = Sedol.IsValid("7108892");   // true

CUSIP - Cusip

CUSIP (Committee on Uniform Securities Identification Procedures, ANSI X9.6) identifies North American financial securities with a 9-character identifier. The check is computed from the 8-character body using a modified Luhn algorithm that handles alphanumeric characters.

using Bodu.IO.Hashing.CheckDigits;

char check = Cusip.Compute("037833100");   // '5'  →  "0378331005"  (Apple)
bool valid  = Cusip.IsValid("0378331005");

LEI - Lei

An LEI (Legal Entity Identifier, ISO 17442) is a 20-character alphanumeric code that uniquely identifies legal entities (companies, funds, etc.) globally. The check uses ISO 7064 MOD 97-10 over the letter-expanded string.

using Bodu.IO.Hashing.CheckDigits;

bool valid = Lei.IsValid("5493000IBP32UQZ0KL24");   // true

Error coverage at a glance

A check digit is only as good as the keying errors it catches. The schemes here cluster by their underlying arithmetic, and the arithmetic dictates coverage:

Family Bodu types Single substitution Adjacent transposition Twin error
Mod 10, weighted sum (Luhn) Luhn, Ean8, Ean13, Gtin14, UpcA, AbaRoutingNumber, Isin All All except 09 ↔ 90 No
Quasigroup (Damm) Damm All All Many
Dihedral D₅ (Verhoeff) Verhoeff All All All
Mod 11 Isbn10, Sedol, Cusip, Iso7064Mod11_2 All Most Some
Mod 97-10 (ISO 7064) Iban, Lei, Iso7064Mod97_10 Effectively all Effectively all Effectively all

The Ean*, Gtin14, UpcA, and Isin schemes share Luhn's mod-10 floor (and its 09 ↔ 90 blind spot) because they reduce to a weighted-sum-mod-10 over the expanded payload. For a free choice of a general decimal identifier, Damm closes that gap with a single character; Verhoeff adds twin-error coverage at the cost of a permutation table. Reach for Luhn only when a standard mandates it. The two-character Iban / Lei schemes over a large modulus catch essentially every realistic transcription error - the appropriate strength for high-value financial identifiers. See the concepts page for what each error class means.

Streaming a payload in chunks

The static Compute / IsValid helpers cover one-shot use, but each type is also a stateful instance you can feed in pieces - useful when the payload arrives across buffers, or when you want to validate then re-read the computed digit:

using Bodu.IO.Hashing.CheckDigits;

var luhn = new Luhn();
luhn.Append("7992");           // ReadOnlySpan<char> overload
luhn.Append('7');              // single-char overload
luhn.Append("39871");
char check = luhn.GetCurrentCheckDigit();   // '3' - non-destructive
luhn.Reset();                               // ready for the next identifier

GetCurrentCheckDigit() (single-char schemes) and GetCurrentCheckDigits() (multi-char schemes) snapshot the running state without consuming it, so you can read the digit and keep appending. Reset() returns the instance to its constructed state for reuse.

Choosing the right algorithm

Identifier Use
Credit card, IMEI, SIN Luhn
Any general decimal identifier where 09 ↔ 90 matters Damm
German ID, medical devices Verhoeff
Retail barcode (13-digit EAN) Ean13
Retail barcode (8-digit EAN) Ean8
Retail barcode (12-digit US/CA) UpcA
Shipping carton barcode (GTIN-14) Gtin14
International securities (ISIN) Isin
North American securities (CUSIP) Cusip
London Stock Exchange securities (SEDOL) Sedol
Bank account number (IBAN) Iban
Book identifier Isbn13 (new) · Isbn10 (legacy)
US bank routing number AbaRoutingNumber
Legal entity identifier (LEI) Lei

Where to go next