Skip to content

Commit 613bc4a

Browse files
committed
Add string.levenshtein
1 parent 7aa70c8 commit 613bc4a

2 files changed

Lines changed: 67 additions & 12 deletions

File tree

docs/string-extensions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,20 @@ print(string.random(10)) -- e.g. "aZ7qT19BcP"
106106

107107
print(string.random(5, "abc")) -- e.g. "abacb"
108108
```
109+
110+
###  `string.levenshtein(first, second)`
111+
112+
Returns the [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) between two strings - the minimum number of edits (insertions, deletions, or subtitutions) required to transform the `first` string into the `second` string.
113+
114+
This is useful for fuzzy matching, typo tolerance, and somewhat measuring similarity between two strings.
115+
116+
> [!NOTE]
117+
> Empty strings return the length of the other string as distance.
118+
119+
```lua
120+
print(string.levenshtein("kitten", "sitting")) -- 3
121+
print(string.levenshtein("cat", "cut")) -- 1
122+
print(string.levenshtein("banana", "banana")) -- 0
123+
```
124+
125+
The function internally builds a distance matrix that tracks how much "effort" it takes to align each character of `first` with each character of `second`. Every mismatch adds a penalty (`+1`), unless the characters match (in which case it is `+0`). The bottom-right cell of the matrix (table) holds the final answer, which is the total number of edits needed.

src/string-extensions.lua

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
--[[lit-meta
22
name = "Richy-Z/string-extensions"
3-
version = "0.1.6"
3+
version = "0.2.0"
44
dependencies = {}
55
description = "Small extensions to Lua's default string library"
66
tags = { "strings", "split", "regex", "random" }
@@ -9,6 +9,10 @@
99
homepage = "https://github.com/Richy-Z/luvit-batteries"
1010
]]
1111

12+
-- yes, I know that 'injecting' my own custom functions into default libraries isn't entirely ideal
13+
-- but its part of actually making the string library more useful
14+
-- also for that built-in feel
15+
1216
local gmatch = string.gmatch
1317
local find = string.find
1418
local sub = string.sub
@@ -19,17 +23,6 @@ local concat = table.concat
1923

2024
local random = math.random
2125

22-
-- yes, I know that 'injecting' my own custom functions into default libraries isn't entirely ideal
23-
-- but its part of actually making the string library more useful
24-
-- also for that built-in feel
25-
26-
27-
28-
29-
30-
31-
32-
3326
-- Returns `true` if `str` starts with `prefix`.
3427
--
3528
-- ```lua
@@ -222,5 +215,50 @@ function string.random(length, charset)
222215
return r
223216
end
224217

218+
-- Returns the [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) between two strings - the minimum number of edits (insertions, deletions, or subtitutions) required to transform the `first` string into the `second` string.
219+
--
220+
-- This is useful for fuzzy matching, typo tolerance, and somewhat measuring similarity between two strings.
221+
--
222+
-- > **NOTE!!!**
223+
-- > Empty strings return the length of the other string as distance.
224+
--
225+
-- ```lua
226+
-- print(string.levenshtein("kitten", "sitting")) -- 3
227+
-- print(string.levenshtein("cat", "cut")) -- 1
228+
-- print(string.levenshtein("banana", "banana")) -- 0
229+
-- ```
230+
--
231+
-- The function internally builds a distance matrix that tracks how much "effort" it takes to align each character of `first` with each character of `second`. Every mismatch adds a penalty (`+1`), unless the characters match (in which case it is `+0`). The bottom-right cell of the matrix (table) holds the final answer, which is the total number of edits needed.
232+
---@param first string
233+
---@param second string
234+
---@return integer distance Levenshtein distance between `first` and `second`
235+
function string.levenshtein(first, second)
236+
local len1, len2 = #first, #second
237+
if len1 == 0 then return len2 end
238+
if len2 == 0 then return len1 end
239+
240+
local matrix = {}
241+
for i = 0, len1 do
242+
matrix[i] = {}
243+
matrix[i][0] = i
244+
end
245+
for j = 0, len2 do
246+
matrix[0][j] = j
247+
end
248+
249+
for i = 1, len1 do
250+
for j = 1, len2 do
251+
local cost = (first:sub(i, i) == second:sub(j, j)) and 0 or 1
252+
matrix[i][j] = math.min(
253+
matrix[i - 1][j] + 1, -- deletion
254+
matrix[i][j - 1] + 1, -- insertion
255+
matrix[i - 1][j - 1] + cost -- substitution
256+
)
257+
end
258+
end
259+
260+
return matrix[len1][len2]
261+
end
262+
225263
-- for legacy compatibility to not break old scripts which rely on running this as a function instead of plain requiring
226264
return function() end

0 commit comments

Comments
 (0)