Skip to Content
LuhnGenerating and Validating

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.

Generating and validating

Luhn.generate computes the check character for an identifier, and Luhn.validate checks if a code carrying one is well-formed. Both return the “phrase” (the cleaned input) they read, allowing you to map a customer’s input back to a database record.

Minting a Check Character with Luhn.generate

Luhn.generate takes an identifier (without a check character) and returns the necessary data to build a valid code.

.('order-2026-0042'); // -> { phrase: 'order20260042', checksum: 'l', filtered: 2 } .('ORDER 2026/0042'); // -> { phrase: 'order20260042', checksum: 'l', filtered: 2 }
Luhn.generate('order-2026-0042')

{ phrase: 'order20260042', checksum: 'l', filtered: 2 }

Retype the phrase, or hyphenate it, and watch the check character.

The Result Object

FieldWhat it holdsExample (order-2026-0042)
phraseThe cleaned input used for the calculation.'order20260042'
checksumThe computed check character.'l'
filteredThe number of characters dropped (separators, etc).2

Because generate reads the phrase, inputs like order-2026-0042 and ORDER 2026/0042 produce the same checksum. This ensures that the identity of the resource is preserved regardless of how it was written.

Handling Empty Inputs

Luhn.generate throws an EmptyInputError when no characters in the input belong to the dictionary.

If every character is dropped, the resulting phrase is empty. Since every empty phrase would produce the same check character, a blank form field or a string of hyphens ('--') would be “valid.” To prevent this, the library requires at least one valid character.

import { , } from '@evanion/luhn'; function (: string): string | null { try { return .().; } catch () { if ( instanceof ) return null; throw ; } } ('order-2026-0042'); // -> 'l' ('--'); // -> null

Validating a Code with Luhn.validate

Luhn.validate treats the last character of the input that belongs to the dictionary as the check character.

.('order-2026-0042-l'); // -> { phrase: 'order20260042l', isValid: true, filtered: 3 } .('ORDER20260042L'); // -> { phrase: 'order20260042l', isValid: true, filtered: 0 } .('order-2026-0043-l'); // -> { phrase: 'order20260043l', isValid: false, filtered: 3 } .('l'); // -> { phrase: 'l', isValid: false, filtered: 0 } .('--'); // -> { phrase: '', isValid: false, filtered: 2 }

Key Validation Rules:

  • The Phrase: validate’s phrase includes the check character.
  • Minimum Length: isValid is false if the phrase contains fewer than two characters (an ID plus its check character is the minimum).
  • Case and Separators: Like generate, validate ignores case and non-dictionary characters.

Issued

accepted
order-2026-0042l is accepted.
Break it

The last character is the check. Break the code and it stops matching.

Issue a code above and try changing a character or swapping two. validate will refuse them immediately.

Mapping a Code Back to an Order

To look up an order using a customer’s input, you should store the phrase generated by Luhn.generate in your database.

When a code comes back, use validate to verify it, then slice off the last character (the check character) to get the original phrase.

function (: string): string | null { const { , } = .(); return ? .(0, -1) : null; } .('order-2026-0042').; // -> 'order20260042' ('ORDER 2026 0042 L'); // -> 'order20260042' ('order-2026-0043-l'); // -> null

This approach ensures that ORDER 2026 0042 L and order-2026-0042-l both resolve to the same database key: order20260042.

Strict Input with filtered

While allowing separators is great for humans, it can be a problem for automated systems. For example, a barcode scanner should not be adding random separators.

You can use the filtered count to enforce strict input. If filtered is greater than 0, you know the input contains characters that weren’t part of the original identifier.

type = 'accepted' | 'misread' | 'unexpected characters'; function (: string): { const { , } = .(); if (!) return 'misread'; if ( > 0) return 'unexpected characters'; return 'accepted'; } ('order20260042l'); // -> 'accepted' ('order-2026-0042-l'); // -> 'unexpected characters' ('order20260043l'); // -> 'misread'

By checking filtered === 0, you can distinguish between a manually typed code (which allows separators) and a scanned code (which must be exact).

Customizing the Alphabet

By default, all calls run on a 36-character dictionary. If you need to use a different set of characters—such as the 32-character alphabet used by @evanion/token—you can build a custom instance. Dictionaries shows how to use createLuhn.

Last updated on