diff --git a/wizard-work/beam/CHECK_DL b/wizard-work/beam/CHECK_DL new file mode 100755 index 0000000..f44d1bc --- /dev/null +++ b/wizard-work/beam/CHECK_DL @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$(readlink -f "$0")")" +python3 test_damerau_levenshtein.py diff --git a/wizard-work/beam/damerau_levenshtein.py b/wizard-work/beam/damerau_levenshtein.py new file mode 100644 index 0000000..028dbb9 --- /dev/null +++ b/wizard-work/beam/damerau_levenshtein.py @@ -0,0 +1,49 @@ +"""Damerau-Levenshtein edit distance (true DL, not OSA).""" +from __future__ import annotations + + +def damerau_levenshtein(a: str, b: str) -> int: + la, lb = len(a), len(b) + if la == 0: + return lb + if lb == 0: + return la + + last = {} + INF = la + lb + d = [[0] * (lb + 2) for _ in range(la + 2)] + d[0][0] = INF + for i in range(1, la + 2): + d[i][0] = INF + d[i][1] = i - 1 + for j in range(1, lb + 2): + d[0][j] = INF + d[1][j] = j - 1 + + for i in range(1, la + 1): + db = 0 + for j in range(1, lb + 1): + i1 = last.get(b[j - 1], 0) + j1 = db + cost = 1 + if a[i - 1] == b[j - 1]: + cost = 0 + db = j + d[i + 1][j + 1] = min( + d[i][j] + cost, + d[i + 1][j] + 1, + d[i][j + 1] + 1, + ) + if i1 > 0 and j1 > 0: + d[i + 1][j + 1] = min( + d[i + 1][j + 1], + d[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1), + ) + last[a[i - 1]] = i + + return d[la + 1][lb + 1] + + +if __name__ == "__main__": + import sys + print(damerau_levenshtein(sys.argv[1], sys.argv[2])) diff --git a/wizard-work/beam/test_damerau_levenshtein.py b/wizard-work/beam/test_damerau_levenshtein.py new file mode 100644 index 0000000..0f97b67 --- /dev/null +++ b/wizard-work/beam/test_damerau_levenshtein.py @@ -0,0 +1,38 @@ +import sys +sys.path.insert(0, "/root/.hermes/wizard-outputs/beam") +from damerau_levenshtein import damerau_levenshtein as dl + +# known values +assert dl("", "") == 0 +assert dl("a", "") == 1 +assert dl("", "a") == 1 +assert dl("ca", "abc") == 2 # transpose? ca->ac is 1 transposition +? +# classic: "CA" -> "ABC": distance? +assert dl("abcd", "abdc") == 1, dl("abcd","abdc") # transposition dc +assert dl("ab", "ba") == 1 +assert dl("abc", "acb") == 1 # transpose b/c +# true DL allows multiple ops on same substring: "CA" -> "ABC" = 2? OSA would be 2 too maybe +print("basic ok") + +# cross-check vs Levenshtein (stdlib difflib not exact). Use simple lev. +def lev(a,b): + if not a: return len(b) + if not b: return len(a) + prev=list(range(len(b)+1)) + for i,ca in enumerate(a,1): + cur=[i] + for j,cb in enumerate(b,1): + cur.append(min(prev[j]+1, cur[-1]+1, prev[j-1]+(ca!=cb))) + prev=cur + return prev[-1] + +import random +random.seed(42) +for _ in range(2000): + n=random.randint(0,8) + m=random.randint(0,8) + a=''.join(random.choice('ab') for _ in range(n)) + b=''.join(random.choice('ab') for _ in range(m)) + # DL <= lev always (more ops allowed) + assert dl(a,b) <= lev(a,b), (a,b,dl(a,b),lev(a,b)) +print("monotonic ok")