Skip to Content
LuhnDictionaries

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 .; // -> true

The 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 .; // -> false

Important: 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.

reasonConstraintWhy it matters
not-a-stringMust be a stringThe library requires a sequence of characters.
too-shortAt least 2 charactersWith only one character, every code would technically be valid.
odd-lengthMust have an even sizeThe doubling step in the Luhn sum requires an even-sized alphabet to avoid “blind spots.”
duplicateNo repeated charactersEach character must map to exactly one unique index.
case-pairsNo 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').; // -> false

Advanced 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 '🂡🂱🃁🃑'.; // -> 8

Caching 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 () === (); // -> true

Matching 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.

Last updated on