The site's sixth approximate match entry, and the first that isn't a dynamic-programming table or a bit-parallel matcher. Damerau–Levenshtein, Banded Edit Distance, Bitap with Edit Distance, and Bitap all count a minimum number of edits. Jaro–Winkler computes a similarity score instead — 0 means nothing alike, 1 means identical — built from three simpler ingredients: which characters can be paired up at all (only within a bounded window of each other), how many paired characters are out of order, and whether the two strings start the same way. It was designed in 1989 by Matthew Jaro for matching person names across U.S. Census records, then extended by William Winkler in 1990 with the prefix bonus below; it's still the default fuzzy-match used for short strings like names, not for long free text.
Fixed pair A = "MARTHA", B = "MARHTA" — the textbook example, chosen
because it shows both mechanisms at once: the T and H are transposed,
and the first three characters MAR match exactly. Press Step or
Run to watch the match, count, and score. In
Jaro-Winkler mode the final score reaches 0.961; switch to
Jaro only and rerun on the identical pair — same matching, same transposition,
but without the prefix bonus the score stops at 0.944.
Three stages, in order:
1. Bounded matching. A character A[i] is allowed to match
B[j] only if |i − j| ≤ window, where
window = floor(max(|A|, |B|) / 2) − 1 (clamped to 0). Scan A left to
right; for each character, take the first still-unmatched equal character in B's
window. This is deliberately not the longest common subsequence or an optimal alignment — it's a
cheap, position-local pairing, which is both the source of its speed and (see Pitfalls) its
blind spot.
2. Transpositions. Line up the matched characters in the order they occur
in A against the order they occur in B — call these compacted
sequences m1 and m2. Where MARTHA/MARHTA
match every character, m1 and m2 happen to equal the original strings;
in general, once some characters go unmatched, they're both shorter than the originals and
skip the gaps. Count positions where m1[k] ≠ m2[k]; each such pair, along with its
mirror, represents one swap, so divide the raw mismatch count by two to get the transposition
count t.
3. The Jaro score weighs how much of each string matched and how orderly the matches were:
jaro = ( m/|A| + m/|B| + (m − t)/m ) / 3
where m is the match count. All three terms are fractions capped at 1, so
jaro itself is always between 0 and 1. Winkler's prefix bonus then
rewards a shared beginning: count the common prefix length L, capped at 4
characters, and boost the score toward 1 by that fraction of what's left:
jw = jaro + L · p · (1 − jaro) // p = 0.1, the standard scaling factor
On MARTHA/MARHTA: all 6 characters match (m = 6), and
comparing the compacted sequences position by position finds exactly one disagreement pair
(T vs. H, both directions), so t = 1. That gives
jaro = (1 + 1 + 5/6) / 3 = 0.9444. The common prefix MAR is 3
characters before T and H disagree, so
jw = 0.9444 + 3 · 0.1 · (1 − 0.9444) = 0.9611.
For a case where matching isn't a clean 1-for-1 with the original order — the general case the
interactive demo's fully-matched example glosses over — take A = "CRATE",
B = "TRACE". Both are 5 characters, so window = floor(5/2) − 1 = 1: each
character can only match within one position of its own index. Scanning left to right,
C (index 0) looks in B[0..1] = "TR" and finds nothing;
R (index 1) looks in B[0..2] and matches B[1] = 'R';
A (index 2) matches B[2]; T (index 4, note E
at index 3 is checked and fails) looks in B[3..4] and finds nothing (the T
it wants is at B[0], outside the window); E matches B[4].
So m1 = "RAE", m2 = "RAE" — identical, zero transpositions — giving
jaro = (3/5 + 3/5 + 3/3)/3 = 0.7333. The C/T swap at
opposite ends of the string never gets counted as a transposition at all; both characters are
simply discarded as unmatched, because the window that makes the algorithm fast is exactly what
keeps it from seeing a rearrangement that far apart.
function jaroSimilarity(a, b) {
if (a === b) return 1;
const la = a.length, lb = b.length;
if (la === 0 || lb === 0) return 0;
const window = Math.max(0, Math.floor(Math.max(la, lb) / 2) - 1);
const matchedA = new Array(la).fill(false);
const matchedB = new Array(lb).fill(false);
let matches = 0;
for (let i = 0; i < la; i++) {
const lo = Math.max(0, i - window), hi = Math.min(lb - 1, i + window);
for (let j = lo; j <= hi; j++) {
if (matchedB[j] || a[i] !== b[j]) continue;
matchedA[i] = matchedB[j] = true;
matches++;
break;
}
}
if (matches === 0) return 0;
const m1 = [], m2 = [];
for (let i = 0; i < la; i++) if (matchedA[i]) m1.push(a[i]);
for (let j = 0; j < lb; j++) if (matchedB[j]) m2.push(b[j]);
let mismatches = 0;
for (let k = 0; k < m1.length; k++) if (m1[k] !== m2[k]) mismatches++;
const transpositions = Math.floor(mismatches / 2);
return (matches / la + matches / lb + (matches - transpositions) / matches) / 3;
}
function jaroWinkler(a, b, p = 0.1, maxPrefix = 4) {
const jaro = jaroSimilarity(a, b);
let prefix = 0;
for (let i = 0; i < Math.min(maxPrefix, a.length, b.length); i++) {
if (a[i] !== b[i]) break;
prefix++;
}
return jaro + prefix * p * (1 - jaro);
}
Checked against three published reference values before writing any of the prose above:
jaroWinkler("MARTHA", "MARHTA") = 0.961, jaroWinkler("DWAYNE", "DUANE")
= 0.840, jaroWinkler("DIXON", "DICKSONX") = 0.813 — all three matched to three
decimal places.
The matching window can hide an obvious rearrangement entirely, treating it as two
plain non-matches instead of a transposition. Demonstrated above:
"CRATE"/"TRACE" swaps C and T across the
whole width of a 5-character string, but the window (1 position either way, for strings this
short) is too narrow to ever compare them, so both are simply discarded as unmatched and the
transposition count stays 0. The resulting score, 0.733, undersells a pair that a person would
probably call "obviously related" — it's exactly the same characters in a different order —
because the algorithm's notion of "different order" only reaches as far as its window.
Jaro–Winkler is not a metric — the triangle inequality can fail. Treating
1 − jw as a distance, a real distance has to satisfy
d(x, z) ≤ d(x, y) + d(y, z): detouring through a third point can never be shorter
than going direct. Checked by brute force over a small word list (512 ordered triples, 28
violations) and confirmed on one clean case:
jw("MARTIN", "MARTHA") = 0.8667 (distance 0.1333),
jw("MARTHA", "ARTHA") = 0.9444 (distance 0.0556) — a detour through
"MARTHA" costs 0.1889 total — but
jw("MARTIN", "ARTHA") = 0.70 directly, a distance of 0.30, which is
higher than the two-step detour. Anything downstream that assumes a proper metric (nearest-neighbor
search with pruning, some clustering algorithms) can misbehave if it's fed Jaro–Winkler distances
without checking this first.
Only a shared beginning is ever rewarded — the same amount of shared content anywhere
else in the string gets nothing. Checked directly: "MARZZZ" and
"MARTHA" share a 3-character prefix (MAR) and nothing else, while
"ZZZTHA" and "MARTHA" share a 3-character suffix (THA) and
nothing else. Both pairs match exactly 3 of 6 characters and land on the identical plain Jaro
score, 0.6667 — the matching and transposition stages don't care where in the string a match
falls. But jaroWinkler("MARZZZ", "MARTHA") = 0.7667 while
jaroWinkler("ZZZTHA", "MARTHA") = 0.6667, unchanged from plain Jaro, because the
prefix scan starts at index 0 and stops at the first mismatch — a shared ending, or a shared
middle, never gets the same look. This is a real design choice, not an oversight: Winkler built
it for U.S. Census name matching, where the first few letters of a surname are unusually
reliable and typos cluster later in the word — a bonus tuned for that pattern doesn't
transfer to text where errors are just as likely at the start.
Time: O(|A| · window) for the matching pass, where
window = O(max(|A|, |B|)) — worst case that's O(|A| · |B|), the same
order as a full dynamic-programming table, but the window only ever needs to be searched, not
filled, so the constant factor is far smaller in practice. The transposition and prefix passes
are both O(min(|A|, |B|)) and don't change the total. Space:
O(|A| + |B|) for the two matched-position arrays, independent of how large the
window is — no table proportional to |A| · |B| is ever allocated.
Jaro–Winkler's niche is short strings where a shared beginning is genuinely informative — names, usernames, product codes, database deduplication — not long free text or anything where the window or the prefix assumption might not hold. For an approach that finds an actual minimum edit count instead of a heuristic score, see Damerau–Levenshtein Distance; for the same "bounded budget" idea applied to exact edit counting, see Banded Edit Distance. For a side-by-side comparison across all eleven approximate-match entries, organized by which problem you actually have, see Choosing an Approximate String Matcher.