Skip to Content
LuhnAPI Reference

API reference

@evanion/luhn 3.0.0 and its patch releases up to 3.0.2, the version on npm. Values on this page were produced by running the source at f9b1d93, a later commit than the 3.0.0 tag, and are not re-executed.

@evanion/luhn exports one factory, the default instance Luhn (also the name of its type), two dictionaries, four other types and three error classes, one heading each below. It runs on Node 20 or newer, as an ES module.

createLuhn

function

Validates options.dictionary and returns a frozen object carrying it, its lookup tables, and the two operations bound to them.

throws {InvalidDictionaryError} when the dictionary fails a constraint.

Full documentation

Everything a dictionary has to satisfy is checked here, once. Nothing is checked at use, so an accepted instance cannot produce a code its own validate rejects.

The example’s 32 characters are the default 36 without i, l, o and w, which are confused when read or heard. LuhnOptions lists the two options and their defaults, and InvalidDictionaryReason lists the five constraints a dictionary can fail.

function (?: LuhnOptions): Luhn;

What the tests state

13 behaviours

  • the constructed instance
    • exposes the dictionary it was given
    • counts n by code point
    • reports uniformOverBytes when the size divides 256
    • is frozen
    • defaults to the 36 lowercase alphanumerics, folding case
    • leaves folding off for a caller-supplied dictionary
  • dictionary constraints
    • rejects a dictionary that is not a string
    • rejects a dictionary with fewer than 2 code points
    • rejects an odd number of code points
    • rejects duplicate code points, naming the repeats
    • rejects folding over a dictionary with case pairs
    • accepts a dictionary with case pairs when not folding
    • counts an astral dictionary by code point, not UTF-16 unit

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { } from '@evanion/luhn'; const = ({ : '0123456789abcdefghjkmnpqrstuvxyz' }); .('a4kp9mx'); // -> { phrase: 'a4kp9mx', checksum: 'a', filtered: 0 } .('a4kp-9mxa').; // -> true .; // -> 32 .; // -> false

Luhn

interface

A dictionary with generate and validate bound to it. The default instance, which is what createLuhn() returns with no options: the 36 lowercase alphanumerics, folding case.

Full documentation

Frozen, so Luhn.dictionary = x throws a TypeError in strict mode, which every ES module runs in, rather than being accepted and ignored. A second dictionary is a second instance, built with createLuhn.

Luhn names the interface in type space and the default instance in value space, so const luhn: Luhn = Luhn compiles. Every instance createLuhn returns carries the same members:

MemberWhat it is
dictionarythe alphabet, as supplied
nthe modulus: the number of code points in the dictionary
caseInsensitivewhether input is folded to lowercase before lookup
uniformOverByteswhether n divides 256, so byte % n picks each character equally often
generate(input)returns a GenerateResult; throws EmptyInputError
validate(input)returns a ValidateResult for a code ending in its check character
interface Luhn { readonly : string; readonly : number; readonly : boolean; readonly : boolean; (: string): GenerateResult; (: string): ValidateResult; }
let : unknown; try { ( as { : string }). = '0123456789'; } catch () { = ; } instanceof ; // -> true .; // -> '0123456789abcdefghijklmnopqrstuvwxyz'

LuhnOptions

interface

What createLuhn builds an instance from. Both fields are optional, and createLuhn() with neither builds what Luhn holds: the 36 lowercase alphanumerics, folding case.

OptionDefault
dictionaryDEFAULT_DICTIONARY
caseInsensitivetrue when dictionary is omitted, false when it is given

caseInsensitive: true over a dictionary holding case pairs throws InvalidDictionaryError with reason: 'case-pairs'.

interface LuhnOptions { ?: string; ?: boolean; }
const = ({ : '0123456789abcdefghjkmnpqrstuvxyz', : true, }); .('A4KP-9MXA').; // -> false .('A4KP-9MXA').; // -> true const = ({ : }); .('Order-2026-0042').; // -> 'F' .('Order-2026-0042-F').; // -> true .('order-2026-0042-F').; // -> false

GenerateResult

interface

The filtered phrase and its check character.

phrase does not include checksum, because the check character did not exist when the phrase was read.

interface GenerateResult { : string; : string; : number; }
.('order-2026-0042'); // -> { phrase: 'order20260042', checksum: 'l', filtered: 2 } .('ORDER 2026/0042'); // -> { phrase: 'order20260042', checksum: 'l', filtered: 2 }

ValidateResult

interface

The filtered phrase and whether its last character checks out.

isValid is false when phrase holds fewer than two code points. phrase includes the check character, because that is what was validated, so the payload without separators is phrase.slice(0, -1).

interface ValidateResult { : string; : boolean; : number; }
.('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 }

DEFAULT_DICTIONARY

constant

36 lowercase alphanumerics, the dictionary createLuhn() uses when the caller supplies none.

Full documentation

It contains no case pairs, which is what makes case folding sound over it: folding maps uppercase input onto a dictionary entry instead of onto a second entry that is already taken.

export declare const = "0123456789abcdefghijklmnopqrstuvwxyz";
import { , } from '@evanion/luhn'; .; // -> '0123456789abcdefghijklmnopqrstuvwxyz' . === ; // -> true .; // -> 36 .; // -> true

ALTERNATING_CASE_DICTIONARY

constant

62 characters, digits followed by Aa Bb Cc …. Case-sensitive only: its case pairs make it invalid under caseInsensitive.

Full documentation

The alternating order decides which index each letter occupies, and therefore every check character the dictionary produces.

createLuhn({ dictionary: ALTERNATING_CASE_DICTIONARY }) reproduces the check characters 2.x produced with static sensitive = true.

export declare const = "0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
let : | undefined; try { ({ : , : true, }); } catch () { if ( instanceof ) = ; } ?.; // -> 'case-pairs' ?..(0, 3); // -> ['Aa', 'Bb', 'Cc']

LuhnError

error

Base class for every error this library throws. Catch it to handle any failure without naming the subclasses.

Full documentation

The name carries the package, so it never collides with another library’s ValidationError in a consumer’s import list.

export declare class extends { constructor(: string); }

What the tests state

1 behaviour

  • LuhnError
    • is an Error

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

import { , } from '@evanion/luhn'; function (: string, : string): string { try { return ({ }).().; } catch () { if ( instanceof ) return .; throw ; } } ('0123456789', '20260042'); // -> '5' ('0123456789', 'ORD'); // -> 'EmptyInputError' ('012345678', '20260042'); // -> 'InvalidDictionaryError'

InvalidDictionaryError

error

Thrown by createLuhn when the dictionary cannot support Luhn mod-N.

Full documentation

One class carrying a reason rather than one class per constraint, so a caller that only wants to know whether a dictionary is acceptable writes one catch arm instead of five.

PropertyWhat it holds
reasonthe InvalidDictionaryReason that failed
dictionarythe dictionary as supplied
offendingthe repeated code points, or the case pairs joined as 'Aa'; empty for the other three
export declare class extends { readonly : ; readonly : string; readonly : readonly string[]; constructor(: , : string, : readonly string[]); }

What the tests state

5 behaviours

  • InvalidDictionaryError
    • carries a name matching the class
    • is a LuhnError
    • keeps the fields it was constructed with
    • says dictionary in the message
    • names the offending code points in the message

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

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: [] }

InvalidDictionaryReason

type

Which construction constraint the dictionary failed.

reasonWhat the dictionary must be
not-a-stringa string
too-shortat least 2 code points long
odd-lengthan even number of code points long
duplicatefree of repeated code points
case-pairsfree of case variants of one letter, when caseInsensitive is true

An odd size is refused because a character at a doubled position contributes floor(2x / n) + (2x mod n) for its index x, and over an odd n two indices contribute the same amount, so a substitution between them goes uncaught.

export type = 'not-a-string' | 'too-short' | 'odd-length' | 'duplicate' | 'case-pairs';

EmptyInputError

error

Thrown by generate when no code point of the input is in the dictionary.

Full documentation

A check character over no payload carries no information, and returning one makes every all-filtered input produce the same result.

EmptyInputError is the only error an instance’s calls raise, and only generate raises it. Every other constraint was settled when the instance was built.

export declare class extends { constructor(: string); }

What the tests state

1 behaviour

  • EmptyInputError
    • is a LuhnError with a name matching the class

Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.

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