feat: add item input now doubles as search bar for the list

This commit is contained in:
2026-08-31 18:02:52 +02:00
parent 3369b49eb6
commit ca0317a01a
3 changed files with 89 additions and 7 deletions
+24
View File
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest'
import { fuzzyMatch } from '../fuzzyMatch'
describe('fuzzyMatch', () => {
it('matches an empty query against anything', () => {
expect(fuzzyMatch('', 'Groceries')).toBe(true)
expect(fuzzyMatch(' ', 'Groceries')).toBe(true)
})
it('matches an exact substring, case-insensitively', () => {
expect(fuzzyMatch('milk', 'Buy milk and eggs')).toBe(true)
expect(fuzzyMatch('MILK', 'buy milk and eggs')).toBe(true)
})
it('tolerates a couple of off characters', () => {
expect(fuzzyMatch('mikl', 'Buy milk and eggs')).toBe(true)
expect(fuzzyMatch('grocries', 'Groceries')).toBe(true)
})
it('does not match unrelated text', () => {
expect(fuzzyMatch('xyz', 'Groceries')).toBe(false)
expect(fuzzyMatch('banana', 'Groceries')).toBe(false)
})
})
+45
View File
@@ -0,0 +1,45 @@
function levenshtein(a: string, b: string): number {
const rows = a.length + 1
const cols = b.length + 1
let prev: number[] = Array.from({ length: cols }, (_, j) => j)
for (let i = 1; i < rows; i++) {
const curr: number[] = [i]
for (let j = 1; j < cols; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
curr.push(Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost))
}
prev = curr
}
return prev[cols - 1] ?? 0
}
function maxDistanceFor(queryLength: number): number {
if (queryLength <= 4) return 1
return 2
}
/**
* Cheap, local fuzzy substring match: true if `text` contains a run of
* characters within edit-distance of `query` (tolerating ~1-2 typos).
*/
export function fuzzyMatch(query: string, text: string): boolean {
const q = query.trim().toLowerCase()
if (!q) return true
const t = text.toLowerCase()
if (t.includes(q)) return true
const maxDistance = maxDistanceFor(q.length)
const minLen = Math.max(1, q.length - maxDistance)
const maxLen = q.length + maxDistance
for (let len = minLen; len <= maxLen; len++) {
for (let start = 0; start + len <= t.length; start++) {
if (levenshtein(q, t.substring(start, start + len)) <= maxDistance) return true
}
}
return false
}