Natural Sort vs Lexicographic Sort

May 28, 20266 min read

If you have ever sorted a list of files and seen file1.txt, file10.txt, file2.txt instead of the expected file1.txt, file2.txt, file10.txt, you have encountered the difference between lexicographic and natural sorting. Lexicographic (alphabetical) sorting compares characters one by one, treating digits like any other character. Natural sorting recognizes numeric sequences and compares them as whole numbers. The distinction matters whenever your data contains mixed text and numbers.

How Lexicographic Sort Works

Lexicographic sorting compares strings character by character based on each character's code point value. When comparing file10 and file2, the algorithm proceeds as follows:

file10 vs file2
──────       (f = f, continue)
 ─────       (i = i, continue)
  ────       (l = l, continue)
   ───       (e = e, continue)
    ──       (1 vs 2 → '1' (49) < '2' (50)) → file10 comes first

The digit 1 has a lower code point than 2, so file10 sorts before file2. This is technically correct lexicographic behavior, but it contradicts human intuition about numerical ordering.

Standard sorting functions in most programming languages default to lexicographic order for strings:

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
files.sort()
// Result: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

How Natural Sort Works

Natural sorting (also called alphanumeric or human sorting) identifies numeric substrings within each string and compares them as integers rather than digit-by-digit. Under natural sort:

file1.txt  →  ["file", 1, ".txt"]
file2.txt  →  ["file", 2, ".txt"]
file10.txt →  ["file", 10, ".txt"]
file20.txt →  ["file", 20, ".txt"]

Each string is decomposed into alternating text and numeric segments. Text segments compare lexicographically; numeric segments compare by integer value. The result matches human expectations:

import { compare } from 'natural-orderby'

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
files.sort(compare())
// Result: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

Where This Difference Matters

File systems and version lists. Filenames with numeric suffixes—patches, chapters, logs, screenshots—are the most common case. A directory listing of screenshots sorted alphabetically shows img1.png through img9.png, then img10.png—unless the viewer uses natural sort.

Part numbers and product codes. Manufacturing part numbers like A-100, A-2, A-10 should typically sort by the numeric component. Lexicographic order scrambles them.

Addresses and room numbers. Street addresses like 221B Baker Street and 10 Downing Street sort differently depending on whether the digit sequences are compared as numbers.

Version strings. Software versions like v1.10 vs v1.9 are a special case. Strict semantic versioning follows its own rules, but a natural sort gets closer than lexicographic order (v1.9 before v1.10), while still not matching full semver semantics.

Implementing Natural Sort

The core algorithm splits each string into tokens of alternating alpha and numeric characters, then compares token-by-token:

function naturalCompare(a, b) {
  const ax = a.split(/(\d+)/)
  const bx = b.split(/(\d+)/)

  for (let i = 0; i < Math.min(ax.length, bx.length); i++) {
    const an = parseInt(ax[i], 10)
    const bn = parseInt(bx[i], 10)

    if (isNaN(an) || isNaN(bn)) {
      // Text token: compare lexicographically
      if (ax[i] < bx[i]) return -1
      if (ax[i] > bx[i]) return 1
    } else {
      // Numeric token: compare as integers
      if (an < bn) return -1
      if (an > bn) return 1
    }
  }

  return ax.length - bx.length
}

const files = ['file1.txt', 'file10.txt', 'file2.txt']
files.sort(naturalCompare)
// Result: ['file1.txt', 'file2.txt', 'file10.txt']

For production use, existing libraries handle edge cases like leading zeros (img001 vs img1), decimal numbers, and locale-specific text comparisons. In JavaScript, natural-orderby and localeCompare with the numeric option are reliable choices:

files.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))

Leading Zeros

A subtle edge case: strings with leading zeros. Natural sort typically treats img001 and img1 as equivalent numerically (both tokenize to 1), falling back to lexicographic comparison of the digit strings themselves. Some implementations offer options to treat leading zeros as significant for tie-breaking.

For quick sorting of any list with natural order, the List Sorter tool supports both lexicographic and natural sort modes out of the box.