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

6.69 Kb · 249 lines
  1// Package levenshtein ports the classic Levenshtein edit-distance algorithm
  2// (as found in Go libraries like agext/levenshtein) to a gno.land realm.
  3//
  4// The core is the textbook dynamic-programming matrix: the minimum number of
  5// single-character insertions, deletions, or substitutions to turn string a
  6// into string b. It is fully rune-aware and pure (deterministic), so it runs
  7// happily on-chain.
  8package levenshtein
  9
 10import (
 11	"strconv"
 12	"strings"
 13)
 14
 15// Distance returns the Levenshtein edit distance between a and b.
 16//
 17// It counts single-rune insertions, deletions, and substitutions and works on
 18// runes (not bytes), so multi-byte UTF-8 input is handled correctly. The
 19// classic two-row DP is used, so memory is O(min(len)) and time is O(len(a)*len(b)).
 20func Distance(a, b string) int {
 21	ra := []rune(a)
 22	rb := []rune(b)
 23
 24	// Keep the shorter slice as the inner (column) dimension.
 25	if len(ra) < len(rb) {
 26		ra, rb = rb, ra
 27	}
 28	n := len(ra)
 29	m := len(rb)
 30	if m == 0 {
 31		return n
 32	}
 33
 34	// prev[j] = distance between ra[:i] and rb[:j].
 35	prev := make([]int, m+1)
 36	for j := 0; j <= m; j++ {
 37		prev[j] = j
 38	}
 39	curr := make([]int, m+1)
 40
 41	for i := 1; i <= n; i++ {
 42		curr[0] = i
 43		for j := 1; j <= m; j++ {
 44			cost := 1
 45			if ra[i-1] == rb[j-1] {
 46				cost = 0
 47			}
 48			curr[j] = min3(
 49				curr[j-1]+1,    // insertion
 50				prev[j]+1,      // deletion
 51				prev[j-1]+cost, // substitution / match
 52			)
 53		}
 54		prev, curr = curr, prev
 55	}
 56	return prev[m]
 57}
 58
 59// Matrix returns the full (len(a)+1) x (len(b)+1) DP matrix used by Distance.
 60// matrix[i][j] is the edit distance between the first i runes of a and the
 61// first j runes of b. The bottom-right cell equals Distance(a, b).
 62func Matrix(a, b string) [][]int {
 63	ra := []rune(a)
 64	rb := []rune(b)
 65	n := len(ra)
 66	m := len(rb)
 67
 68	d := make([][]int, n+1)
 69	for i := 0; i <= n; i++ {
 70		d[i] = make([]int, m+1)
 71		d[i][0] = i
 72	}
 73	for j := 0; j <= m; j++ {
 74		d[0][j] = j
 75	}
 76	for i := 1; i <= n; i++ {
 77		for j := 1; j <= m; j++ {
 78			cost := 1
 79			if ra[i-1] == rb[j-1] {
 80				cost = 0
 81			}
 82			d[i][j] = min3(d[i][j-1]+1, d[i-1][j]+1, d[i-1][j-1]+cost)
 83		}
 84	}
 85	return d
 86}
 87
 88// Similarity returns a 0..100 percentage of how similar a and b are, defined as
 89// (1 - distance/maxLen) * 100 rounded to the nearest integer. Two empty strings
 90// are considered 100% similar.
 91func Similarity(a, b string) int {
 92	la := len([]rune(a))
 93	lb := len([]rune(b))
 94	maxLen := la
 95	if lb > maxLen {
 96		maxLen = lb
 97	}
 98	if maxLen == 0 {
 99		return 100
100	}
101	dist := Distance(a, b)
102	// Rounded percentage of matching characters.
103	return ((maxLen-dist)*100 + maxLen/2) / maxLen
104}
105
106func min3(a, b, c int) int {
107	m := a
108	if b < m {
109		m = b
110	}
111	if c < m {
112		m = c
113	}
114	return m
115}
116
117// renderTable formats the DP matrix as a Markdown table with a and b as headers.
118func renderTable(a, b string) string {
119	ra := []rune(a)
120	rb := []rune(b)
121	d := Matrix(a, b)
122
123	var sb strings.Builder
124	// Header row: blank | "" | each rune of b.
125	sb.WriteString("|   | ε |")
126	for _, r := range rb {
127		sb.WriteString(" `")
128		sb.WriteString(string(r))
129		sb.WriteString("` |")
130	}
131	sb.WriteString("\n")
132	// Separator.
133	sb.WriteString("|---|")
134	for j := 0; j <= len(rb); j++ {
135		sb.WriteString("---|")
136	}
137	sb.WriteString("\n")
138	// Data rows.
139	for i := 0; i <= len(ra); i++ {
140		if i == 0 {
141			sb.WriteString("| **ε** |")
142		} else {
143			sb.WriteString("| **`")
144			sb.WriteString(string(ra[i-1]))
145			sb.WriteString("`** |")
146		}
147		for j := 0; j <= len(rb); j++ {
148			sb.WriteString(" ")
149			sb.WriteString(strconv.Itoa(d[i][j]))
150			sb.WriteString(" |")
151		}
152		sb.WriteString("\n")
153	}
154	return sb.String()
155}
156
157// Render implements the gnoweb view.
158//
159//   - "" or "/"            : explanation + examples.
160//   - "/<a>/<b>"           : distance between a and b, with the DP table.
161func Render(path string) string {
162	path = strings.TrimPrefix(path, "/")
163	if path == "" {
164		return renderHome()
165	}
166
167	parts := strings.SplitN(path, "/", 2)
168	if len(parts) != 2 {
169		var sb strings.Builder
170		sb.WriteString("# Levenshtein\n\n")
171		sb.WriteString("Provide two words as `/<a>/<b>`, e.g. [`/kitten/sitting`](/r/moul/x/daily/levenshtein:kitten/sitting).\n\n")
172		sb.WriteString("[← back](/r/moul/x/daily/levenshtein)\n")
173		return sb.String()
174	}
175
176	a := parts[0]
177	b := parts[1]
178	dist := Distance(a, b)
179	sim := Similarity(a, b)
180
181	var sb strings.Builder
182	sb.WriteString("# Levenshtein distance\n\n")
183	sb.WriteString("Transforming **`")
184	sb.WriteString(a)
185	sb.WriteString("`** → **`")
186	sb.WriteString(b)
187	sb.WriteString("`**\n\n")
188	sb.WriteString("- **Edit distance:** `")
189	sb.WriteString(strconv.Itoa(dist))
190	sb.WriteString("` single-character edits (insert / delete / substitute)\n")
191	sb.WriteString("- **Similarity:** `")
192	sb.WriteString(strconv.Itoa(sim))
193	sb.WriteString("%`\n\n")
194
195	sb.WriteString("## DP table\n\n")
196	sb.WriteString("Each cell `d[i][j]` is the distance between the first *i* runes of `")
197	sb.WriteString(a)
198	sb.WriteString("` and the first *j* runes of `")
199	sb.WriteString(b)
200	sb.WriteString("`. The bottom-right cell is the answer.\n\n")
201	sb.WriteString(renderTable(a, b))
202	sb.WriteString("\n[← back](/r/moul/x/daily/levenshtein)\n")
203	return sb.String()
204}
205
206func renderHome() string {
207	var sb strings.Builder
208	sb.WriteString("# Levenshtein edit distance\n\n")
209	sb.WriteString("The **Levenshtein distance** between two strings is the minimum number of ")
210	sb.WriteString("single-character edits — *insertions*, *deletions*, or *substitutions* — ")
211	sb.WriteString("needed to turn one string into the other. This realm reimplements the classic ")
212	sb.WriteString("dynamic-programming algorithm (à la Go's `agext/levenshtein`) fully rune-aware and on-chain.\n\n")
213
214	sb.WriteString("## Try it\n\n")
215	sb.WriteString("Append two words as `/<a>/<b>`:\n\n")
216	examples := [][2]string{
217		{"kitten", "sitting"},
218		{"flaw", "lawn"},
219		{"sunday", "saturday"},
220		{"gno", "gnoland"},
221	}
222	for _, ex := range examples {
223		a, b := ex[0], ex[1]
224		d := Distance(a, b)
225		sb.WriteString("- [`/")
226		sb.WriteString(a)
227		sb.WriteString("/")
228		sb.WriteString(b)
229		sb.WriteString("`](/r/moul/x/daily/levenshtein:")
230		sb.WriteString(a)
231		sb.WriteString("/")
232		sb.WriteString(b)
233		sb.WriteString(") → distance **")
234		sb.WriteString(strconv.Itoa(d))
235		sb.WriteString("**\n")
236	}
237
238	sb.WriteString("\n## The classic example\n\n")
239	sb.WriteString("`kitten` → `sitting` = **3**:\n\n")
240	sb.WriteString("1. `kitten` → `sitten` (substitute *k* → *s*)\n")
241	sb.WriteString("2. `sitten` → `sittin` (substitute *e* → *i*)\n")
242	sb.WriteString("3. `sittin` → `sitting` (insert *g* at the end)\n\n")
243
244	sb.WriteString("## API\n\n")
245	sb.WriteString("- `Distance(a, b string) int` — the edit distance.\n")
246	sb.WriteString("- `Matrix(a, b string) [][]int` — the full DP matrix.\n")
247	sb.WriteString("- `Similarity(a, b string) int` — a 0..100 similarity percentage.\n")
248	return sb.String()
249}