Prompt
Implement Levenshtein distance in JavaScript.
Requirements:
- Export a single function `levenshtein(a, b)` that returns the edit distance (number of single-character insertions, deletions, or substitutions) between two strings.
- Handle empty strings correctly.
- Use a space-efficient implementation (two rows, not a full matrix) where reasonable.
- Include a test file using plain Node.js `assert` (no external test framework) that covers: identical strings, one empty string, both empty, single-character difference, insertions, deletions, substitutions, and a longer realistic case (e.g. "kitten" vs "sitting" → 3).
Return two code blocks: one for `levenshtein.js`, one for `levenshtein.test.js`. Briefly note the time and space complexity.
Responses with Levenshtein JS — Sonnet 4.6 vs Devstral vs Opus 4.7 (1 rows)
claude-sonnet-4.6
anthropic/claude-sonnet-4.6
35.57sSlowest by 4x$0.035
Here are both files:
levenshtein.js
/**
* Computes the Levenshtein (edit) distance between two strings.
*
* Time complexity: O(m * n) — every cell of the virtual matrix is visited once.
* Space complexity: O(min(m, n)) — only two rows are kept in memory at any time.
*
* @param {string} a
* @param {string} b
* @returns {number} minimum edit distance
*/
function levenshtein(a, b) {
if (typeof a !== "string" || typeof b !== "string") {
throw new TypeError("Both arguments must be strings");
}
// Ensure 'a' is the shorter string so the two arrays stay as small as possible.
if (a.length > b.length) {
[a, b] = [b, a];
}
const m = a.length;
const n = b.length;
// Base cases — one or both strings are empty.
if (m === 0) return n;
if (n === 0) return m;
// prev[j] = distance between a[0..i-1] and b[0..j-1] (previous row)
// curr[j] = distance being computed for the current row i
let prev = new Array(m + 1);
let curr = new Array(m + 1);
// Initialise the first row: transforming "" → a[0..j-1] costs j insertions.
for (let j = 0; j <= m; j++) {
prev[j] = j;
}
for (let i = 1; i <= n; i++) {
// Transforming a[0..i-1] → "" costs i deletions.
curr[0] = i;
for (let j = 1; j <= m; j++) {
const substitutionCost = a[j - 1] === b[i - 1] ? 0 : 1;
curr[j] = Math.min(
prev[j] + 1, // deletion (remove b[i-1])
curr[j - 1] + 1, // insertion (insert a[j-1])
prev[j - 1] + substitutionCost // substitution / match
);
}
// Swap rows — curr becomes the new prev for the next iteration.
[prev, curr] = [curr, prev];
}
// After the final swap, prev holds the last completed row.
return prev[m];
}
module.exports = { levenshtein };
levenshtein.test.js
"use strict";
const assert = require("assert");
const { levenshtein } = require("./levenshtein");
// ─── helpers ────────────────────────────────────────────────────────────────
let passed = 0;
let failed = 0;
function test(description, fn) {
try {
fn();
console.log(` ✓ ${description}`);
passed++;
} catch (err) {
console.error(` ✗ ${description}`);
console.error(` ${err.message}`);
failed++;
}
}
function assertEqual(actual, expected, hint = "") {
assert.strictEqual(
actual,
expected,
`Expected ${expected}, got ${actual}${hint ? " — " + hint : ""}`
);
}
// ─── test suites ─────────────────────────────────────────────────────────────
console.log("\nLevenshtein distance tests\n");
// 1. Both strings identical
console.log("Identical strings");
test("empty string vs empty string → 0", () =>
assertEqual(levenshtein("", ""), 0));
test('"abc" vs "abc" → 0', () =>
assertEqual(levenshtein("abc", "abc"), 0));
test("single char vs itself → 0", () =>
assertEqual(levenshtein("z", "z"), 0));
// 2. One or both strings empty
console.log("\nEmpty string edge cases");
test('"" vs "abc" → 3 (3 insertions)', () =>
assertEqual(levenshtein("", "abc"), 3));
test('"abc" vs "" → 3 (3 deletions)', () =>
assertEqual(levenshtein("abc", ""), 3));
test('"" vs "x" → 1', () =>
assertEqual(levenshtein("", "x"), 1));
test('"x" vs "" → 1', () =>
assertEqual(levenshtein("x", ""), 1));
// 3. Single-character differences
console.log("\nSingle-character differences");
test('"a" vs "b" → 1 (substitution)', () =>
assertEqual(levenshtein("a", "b"), 1));
test('"ab" vs "a" → 1 (deletion)', () =>
assertEqual(levenshtein("ab", "a"), 1));
test('"a" vs "ab" → 1 (insertion)', () =>
assertEqual(levenshtein("a", "ab"), 1));
// 4. Pure insertions
console.log("\nInsertions");
test('"abc" vs "aXbYc" → 2', () =>
assertEqual(levenshtein("abc", "aXbYc"), 2));
test('"abc" vs "xabc" → 1 (prepend)', () =>
assertEqual(levenshtein("abc", "xabc"), 1));
test('"abc" vs "abcx" → 1 (append)', () =>
assertEqual(levenshtein("abc", "abcx"), 1));
// 5. Pure deletions
console.log("\nDeletions");
test('"aXbYc" vs "abc" → 2', () =>
assertEqual(levenshtein("aXbYc", "abc"), 2));
test('"xabc" vs "abc" → 1', () =>
assertEqual(levenshtein("xabc", "abc"), 1));
// 6. Pure substitutions
console.log("\nSubstitutions");
test('"abc" vs "axc" → 1', () =>
assertEqual(levenshtein("abc", "axc"), 1));
test('"abc" vs "xyz" → 3 (all different)', () =>
assertEqual(levenshtein("abc", "xyz"), 3));
test('"saturday" vs "sunday" → 3', () =>
assertEqual(levenshtein("saturday", "sunday"), 3));
// 7. Realistic longer cases
console.log("\nLonger / realistic cases");
test('"kitten" vs "sitting" → 3', () =>
assertEqual(levenshtein("kitten", "sitting"), 3));
test('"intention" vs "execution" → 5', () =>
assertEqual(levenshtein("intention", "execution"), 5));
test('"rosetta" vs "racecar" → 6', () =>
assertEqual(levenshtein("rosetta", "racecar"), 6));
// 8. Symmetry property
console.log("\nSymmetry");
const pairs = [["kitten", "sitting"], ["abc", "xyz"], ["", "hello"]];
pairs.forEach(([x, y]) => {
test(`levenshtein("${x}", "${y}") === levenshtein("${y}", "${x}")`, () =>
assertEqual(levenshtein(x, y), levenshtein(y, x)));
});
// 9. Type guard
console.log("\nInput validation");
test("throws TypeError for non-string first argument", () =>
assert.throws(() => levenshtein(42, "abc"), TypeError));
test("throws TypeError for non-string second argument", () =>
assert.throws(() => levenshtein("abc", null), TypeError));
// ─── summary ─────────────────────────────────────────────────────────────────
console.log(`\n${"─".repeat(40)}`);
console.log(`Results: ${passed} passed, ${failed} failed\n`);
if (failed > 0) process.exit(1);
Complexity notes
| Dimension | Value | Reasoning |
|---|---|---|
| Time | O(m × n) | Every cell of the conceptual (m+1)×(n+1) DP table is computed exactly once |
| Space | O(min(m, n)) | Only two rolling arrays of length min(m,n)+1 are kept; the longer string is always iterated in the outer loop |
Key design decisions
- Row swapping — the two arrays are swapped via destructuring at the end of each outer iteration, avoiding any allocation inside the hot loop.
- Shorter string as columns — by ensuring
ais always the shorter string, the two arrays are as small as possible, which improves cache behaviour for very long strings. - No external dependencies — the test file uses only Node's built-in
assertmodule and a minimal hand-rolledtest()helper, sonode levenshtein.test.jsis all that is needed to run it.
devstral-2512
mistralai/devstral-2512
8.85sFastest by 4x$0.0015Cheapest by 30x
Here are the implementations for levenshtein.js and levenshtein.test.js:
levenshtein.js
function levenshtein(a, b) {
if (a === b) return 0;
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
// Use two rows to save space
let prevRow = Array(b.length + 1).fill(0);
let currRow = Array(b.length + 1).fill(0);
// Initialize first row
for (let j = 0; j <= b.length; j++) {
prevRow[j] = j;
}
for (let i = 1; i <= a.length; i++) {
currRow[0] = i; // Cost of deleting all characters up to i
for (let j = 1; j <= b.length; j++) {
const deletionCost = prevRow[j] + 1;
const insertionCost = currRow[j - 1] + 1;
const substitutionCost = prevRow[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
currRow[j] = Math.min(deletionCost, insertionCost, substitutionCost);
}
// Swap rows for next iteration
[prevRow, currRow] = [currRow, prevRow];
currRow.fill(0); // Reset current row
}
return prevRow[b.length];
}
module.exports = levenshtein;
levenshtein.test.js
const assert = require('assert');
const levenshtein = require('./levenshtein');
assert.strictEqual(levenshtein('kitten', 'sitting'), 3);
assert.strictEqual(levenshtein('hello', 'hello'), 0);
assert.strictEqual(levenshtein('', 'abc'), 3);
assert.strictEqual(levenshtein('abc', ''), 3);
assert.strictEqual(levenshtein('', ''), 0);
assert.strictEqual(levenshtein('a', 'b'), 1);
assert.strictEqual(levenshtein('abc', 'ab'), 1); // deletion
assert.strictEqual(levenshtein('ab', 'abc'), 1); // insertion
assert.strictEqual(levenshtein('abc', 'adc'), 1); // substitution
assert.strictEqual(levenshtein('saturday', 'sunday'), 3);
console.log('All tests passed!');
Complexity Analysis:
- Time Complexity: O(m*n), where m and n are the lengths of the two strings. This is because we need to fill a matrix of size m×n (though we optimize space by using two rows).
- Space Complexity: O(min(m, n)), since we only keep two rows in memory at any time (each of length n+1). This is more space-efficient than the full O(m*n) matrix approach.
The implementation handles all edge cases (empty strings, identical strings) and uses a space-optimized approach with two rows. The test file covers all the required cases.
claude-opus-4.7
anthropic/claude-opus-4.7
18.75s$0.045Priciest by 30x
Levenshtein Distance Implementation
Complexity:
- Time: O(m × n) where m and n are the lengths of the two strings.
- Space: O(min(m, n)) — we keep only two rows and ensure the shorter string indexes the row.
// levenshtein.js
'use strict';
function levenshtein(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') {
throw new TypeError('Both arguments must be strings');
}
if (a === b) return 0;
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
// Ensure `b` is the shorter string so the rows have length min(m, n) + 1
if (a.length < b.length) {
const tmp = a;
a = b;
b = tmp;
}
const m = a.length;
const n = b.length;
let prev = new Array(n + 1);
let curr = new Array(n + 1);
for (let j = 0; j <= n; j++) prev[j] = j;
for (let i = 1; i <= m; i++) {
curr[0] = i;
const ac = a.charCodeAt(i - 1);
for (let j = 1; j <= n; j++) {
const cost = ac === b.charCodeAt(j - 1) ? 0 : 1;
const del = prev[j] + 1;
const ins = curr[j - 1] + 1;
const sub = prev[j - 1] + cost;
let min = del < ins ? del : ins;
if (sub < min) min = sub;
curr[j] = min;
}
// Swap rows
const tmp = prev;
prev = curr;
curr = tmp;
}
return prev[n];
}
module.exports = levenshtein;
// levenshtein.test.js
'use strict';
const assert = require('assert');
const levenshtein = require('./levenshtein');
// Identical strings
assert.strictEqual(levenshtein('', ''), 0, 'both empty');
assert.strictEqual(levenshtein('abc', 'abc'), 0, 'identical strings');
assert.strictEqual(levenshtein('hello world', 'hello world'), 0, 'identical with space');
// One empty string
assert.strictEqual(levenshtein('', 'abc'), 3, 'first empty');
assert.strictEqual(levenshtein('abc', ''), 3, 'second empty');
assert.strictEqual(levenshtein('', 'x'), 1, 'one empty, one char');
// Single-character difference (substitution)
assert.strictEqual(levenshtein('a', 'b'), 1, 'single char substitution');
assert.strictEqual(levenshtein('cat', 'bat'), 1, 'one substitution');
assert.strictEqual(levenshtein('cat', 'car'), 1, 'one substitution end');
// Insertions
assert.strictEqual(levenshtein('cat', 'cats'), 1, 'single insertion at end');
assert.strictEqual(levenshtein('cat', 'chat'), 1, 'single insertion in middle');
assert.strictEqual(levenshtein('abc', 'abcdef'), 3, 'multiple insertions');
// Deletions
assert.strictEqual(levenshtein('cats', 'cat'), 1, 'single deletion at end');
assert.strictEqual(levenshtein('chat', 'cat'), 1, 'single deletion in middle');
assert.strictEqual(levenshtein('abcdef', 'abc'), 3, 'multiple deletions');
// Substitutions
assert.strictEqual(levenshtein('abc', 'xyz'), 3, 'all substituted');
assert.strictEqual(levenshtein('book', 'back'), 2, 'two substitutions');
// Mixed operations (longer realistic cases)
assert.strictEqual(levenshtein('kitten', 'sitting'), 3, 'kitten -> sitting');
assert.strictEqual(levenshtein('sitting', 'kitten'), 3, 'symmetry: sitting -> kitten');
assert.strictEqual(levenshtein('Saturday', 'Sunday'), 3, 'Saturday -> Sunday');
assert.strictEqual(levenshtein('flaw', 'lawn'), 2, 'flaw -> lawn');
assert.strictEqual(levenshtein('intention', 'execution'), 5, 'intention -> execution');
// Symmetry property checks
assert.strictEqual(
levenshtein('algorithm', 'altruistic'),
levenshtein('altruistic', 'algorithm'),
'symmetry property'
);
// Type checking
assert.throws(() => levenshtein(123, 'abc'), TypeError, 'throws on non-string');
assert.throws(() => levenshtein('abc', null), TypeError, 'throws on null');
console.log('All Levenshtein tests passed ✓');
Run with:
node levenshtein.test.js
Discover if you're paying too much for an AI model that's too slow.