Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

levenshtein.gno

2.78 Kb · 114 lines
  1// Package levenshtein ports the classic Levenshtein edit-distance algorithm
  2// (as found in Go libraries like agext/levenshtein) to gno — as a reusable pure
  3// package.
  4//
  5// The core is the textbook dynamic-programming matrix: the minimum number of
  6// single-character insertions, deletions, or substitutions to turn string a
  7// into string b. It is fully rune-aware and pure (deterministic), so it runs
  8// happily on-chain.
  9//
 10// A live demo of this package (an interactive distance calculator with the DP
 11// table) is at [r/moul/x/daily/levenshteindemo](/r/moul/x/daily/levenshteindemo/v0).
 12package levenshtein
 13
 14// Distance returns the Levenshtein edit distance between a and b.
 15//
 16// It counts single-rune insertions, deletions, and substitutions and works on
 17// runes (not bytes), so multi-byte UTF-8 input is handled correctly. The
 18// classic two-row DP is used, so memory is O(min(len)) and time is O(len(a)*len(b)).
 19func Distance(a, b string) int {
 20	ra := []rune(a)
 21	rb := []rune(b)
 22
 23	// Keep the shorter slice as the inner (column) dimension.
 24	if len(ra) < len(rb) {
 25		ra, rb = rb, ra
 26	}
 27	n := len(ra)
 28	m := len(rb)
 29	if m == 0 {
 30		return n
 31	}
 32
 33	// prev[j] = distance between ra[:i] and rb[:j].
 34	prev := make([]int, m+1)
 35	for j := 0; j <= m; j++ {
 36		prev[j] = j
 37	}
 38	curr := make([]int, m+1)
 39
 40	for i := 1; i <= n; i++ {
 41		curr[0] = i
 42		for j := 1; j <= m; j++ {
 43			cost := 1
 44			if ra[i-1] == rb[j-1] {
 45				cost = 0
 46			}
 47			curr[j] = min3(
 48				curr[j-1]+1,    // insertion
 49				prev[j]+1,      // deletion
 50				prev[j-1]+cost, // substitution / match
 51			)
 52		}
 53		prev, curr = curr, prev
 54	}
 55	return prev[m]
 56}
 57
 58// Matrix returns the full (len(a)+1) x (len(b)+1) DP matrix used by Distance.
 59// matrix[i][j] is the edit distance between the first i runes of a and the
 60// first j runes of b. The bottom-right cell equals Distance(a, b).
 61func Matrix(a, b string) [][]int {
 62	ra := []rune(a)
 63	rb := []rune(b)
 64	n := len(ra)
 65	m := len(rb)
 66
 67	d := make([][]int, n+1)
 68	for i := 0; i <= n; i++ {
 69		d[i] = make([]int, m+1)
 70		d[i][0] = i
 71	}
 72	for j := 0; j <= m; j++ {
 73		d[0][j] = j
 74	}
 75	for i := 1; i <= n; i++ {
 76		for j := 1; j <= m; j++ {
 77			cost := 1
 78			if ra[i-1] == rb[j-1] {
 79				cost = 0
 80			}
 81			d[i][j] = min3(d[i][j-1]+1, d[i-1][j]+1, d[i-1][j-1]+cost)
 82		}
 83	}
 84	return d
 85}
 86
 87// Similarity returns a 0..100 percentage of how similar a and b are, defined as
 88// (1 - distance/maxLen) * 100 rounded to the nearest integer. Two empty strings
 89// are considered 100% similar.
 90func Similarity(a, b string) int {
 91	la := len([]rune(a))
 92	lb := len([]rune(b))
 93	maxLen := la
 94	if lb > maxLen {
 95		maxLen = lb
 96	}
 97	if maxLen == 0 {
 98		return 100
 99	}
100	dist := Distance(a, b)
101	// Rounded percentage of matching characters.
102	return ((maxLen-dist)*100 + maxLen/2) / maxLen
103}
104
105func min3(a, b, c int) int {
106	m := a
107	if b < m {
108		m = b
109	}
110	if c < m {
111		m = c
112	}
113	return m
114}