Unreleased changes
These pages document main, which has changes 3.0.2 does not. npm install @evanion/luhn gives you 3.0.2, documented here. Values on these pages are produced by running the current source on every build.
Dictionaries
createLuhn({ dictionary }) builds an instance over a set of characters of your choice. This allows you to adapt the Luhn algorithm to any alphabet—from credit card digits to a custom set of symbols.
The Foundation: The Default Dictionary
The standard Luhn instance is built over the DEFAULT_DICTIONARY, which consists of the 36 lowercase alphanumerics (0-9 and a-z).
import { , } from '@evanion/luhn';
.; // -> '0123456789abcdefghijklmnopqrstuvwxyz'
. === ; // -> true
.; // -> 36
.; // -> trueThe default instance automatically folds input to lowercase. This ensures that O is treated as o, mapping it to a single consistent index in the dictionary.
Building Custom Instances with createLuhn
When your identifiers use a different alphabet, you can build a custom Luhn instance.
For example, the @evanion/token package uses a 32-character dictionary that drops “confusable” characters (i, l, o, w) to prevent human transcription errors. You can create a similar instance manually:
import { } from '@evanion/luhn';
const = ({ : '0123456789abcdefghjkmnpqrstuvxyz' });
.('a4kp9mx'); // -> { phrase: 'a4kp9mx', checksum: 'a', filtered: 0 }
.('a4kp-9mxa').; // -> true
.; // -> 32
.; // -> falseImportant: The Importance of Order
The dictionary string is not just a set of characters—it is an ordered list. The index of each character in the string determines its value in the Luhn sum.
Never sort or deduplicate your dictionary string at runtime. If you change the order of characters, you change the check characters the instance produces, which will break validation for any codes already issued.
Dictionary Constraints
To ensure the Luhn algorithm works correctly, createLuhn validates your dictionary against five constraints. If any are broken, it throws an InvalidDictionaryError.
reason | Constraint | Why it matters |
|---|---|---|
not-a-string | Must be a string | The library requires a sequence of characters. |
too-short | At least 2 characters | With only one character, every code would technically be valid. |
odd-length | Must have an even size | The doubling step in the Luhn sum requires an even-sized alphabet to avoid “blind spots.” |
duplicate | No repeated characters | Each character must map to exactly one unique index. |
case-pairs | No case variants (if caseInsensitive: true) | If A and a both exist, folding input to lowercase would make the index of A unreachable. |
import { , } from '@evanion/luhn';
function (: string) {
try {
({ });
return null;
} catch () {
if (!( instanceof )) throw ;
return { : ., : . };
}
}
('0123456789abcdefghjkmnpqrstuvxyz'); // -> null
('0123456789abcdefghjkmnpqrstuvxy'); // -> { reason: 'odd-length', offending: [] }
('0123456789abcdefghjkmnpqrstuvxyy'); // -> { reason: 'duplicate', offending: ['y'] }
('0'); // -> { reason: 'too-short', offending: [] }Handling Case Sensitivity
By default, createLuhn sets caseInsensitive to false when you provide a custom dictionary. This is because the library cannot assume you want your input folded to lowercase.
If you want your custom instance to ignore case, set caseInsensitive: true. This is useful for dictionaries that only contain lowercase letters but need to accept uppercase input from users.
const = ({
: '0123456789abcdefghjkmnpqrstuvxyz',
: true,
});
.('A4KP-9MXA').; // -> false
.('A4KP-9MXA').; // -> true
const = ({ : });
.('Order-2026-0042').; // -> 'F'
.('Order-2026-0042-F').; // -> true
.('order-2026-0042-F').; // -> falseAdvanced Considerations
Unicode and Code Points
createLuhn counts code points, not UTF-16 units. This means you can safely use astral symbols such as 🂡 in your dictionary without breaking the index mapping. An emoji built from several code points, such as ❤️ or a flag, takes several entries.
const = ({ : '🂡🂱🃁🃑' });
.; // -> 4
'🂡🂱🃁🃑'.; // -> 8Caching Instances
createLuhn builds a lookup table for the dictionary every time it is called. If your application uses a few recurring dictionaries (e.g., one for orders, one for pickup codes), you should cache the resulting instances in a Map rather than recreating them on every request.
const = '0123456789abcdefghjkmnpqrstuvxyz';
const = new <string, Luhn>();
function (: string): Luhn {
const = .() ?? ({ });
.(, );
return ;
}
().('a4kp-9mxa').; // -> true
() === (); // -> trueMatching Published Standards
Different industries use different dictionaries for their checksums. Standards and modulo bias explains which dictionaries match published standards and how to avoid “modulo bias” when generating random codes.