How do I compare strings for fuzzy or approximate matching in EK9?

← Common String Operations · Ref: Q179

EK9 has a built-in fuzzy-match operator for strings. The <~> operator returns a weighted, Levenshtein-style match COST as an Integer: lower means closer, 0 means identical. It is not the plain edit count.

FUZZY MATCH COST (<~>)

  cost <- str1 <~> str2

The weights: a change of case only costs 1; a substitution, an insertion or a deletion usually costs 2. When the two strings share too few characters to be a plausible match (including comparing with ""), the result is the Integer maximum, 2147483647, meaning 'no viable match'.

IDENTICAL STRINGS

Comparing "hello" with "hello" gives 0.

ONE SUBSTITUTION

Comparing "kitten" with "sitten" gives 2.

SEVERAL EDITS

Comparing "kitten" with "sitting" gives 6 (the classic edit count is 3).

PRACTICAL USE: TYPO DETECTION

Pick a threshold in COST units, not edits. A transposed pair ("funciton" for "function") costs 4:

  if (input <~> expected) <= 4
    stdout.println("Close enough")

PRACTICAL USE: CLOSEST MATCH

Compare the input with each candidate and keep the lowest cost: "colr" scores 2 against "color", 4 against "collar", and the Integer maximum against "column".

REGEX FOR PATTERN MATCHING

For pattern-based matching rather than distance:

  if myString matches /pattern/

See Q37 for string basics. See Q33 for regular expressions.

See Q37 for strings. See Q33 for regular expressions. See Q239 for the <~> fuzzy comparison operator used in ordering and matching.

Example

defines module qa.stringops.fuzzymatch

  defines program
    FuzzyMatchDemo()
      stdout <- Stdout()

      // === FUZZY MATCH COST (<~>) ===

      // Identical strings — cost 0
      greeting <- "hello"
      sameGreeting <- "hello"
      dist0 <- greeting <~> sameGreeting
      stdout.println(`hello <~> hello = ${dist0}`)

      // One substitution — cost 2
      original <- "kitten"
      dist1 <- original <~> "sitten"
      stdout.println(`kitten <~> sitten = ${dist1}`)

      // Several edits — cost 6 (the classic edit count would be 3)
      dist3 <- original <~> "sitting"
      stdout.println(`kitten <~> sitting = ${dist3}`)

      // Too few shared characters — Integer maximum (no viable match)
      planet <- "world"
      distEmpty <- planet <~> ""
      stdout.println(`world <~> empty = ${distEmpty}`)

      // === PRACTICAL: TYPO DETECTION ===

      expected <- "function"
      typo <- "funciton"
      typoDistance <- expected <~> typo
      stdout.println(`Typo distance: ${typoDistance}`)

      // A transposition costs 4, so the threshold is in cost units
      typoThreshold <- 4
      if typoDistance <= typoThreshold
        stdout.println("Close enough - likely a typo")

      // === PRACTICAL: FIND CLOSEST MATCH ===

      input <- "colr"
      target1 <- "color"
      target2 <- "collar"
      target3 <- "column"

      d1 <- input <~> target1
      d2 <- input <~> target2
      d3 <- input <~> target3
      stdout.println(`colr <~> color = ${d1}`)
      stdout.println(`colr <~> collar = ${d2}`)
      stdout.println(`colr <~> column = ${d3}`)

      // The lowest cost is the closest match
      if d1 < d2 and d1 < d3
        stdout.println(`Closest to colr: ${target1}`)

      // === REGEX FOR PATTERN MATCHING ===

      testPhrase <- "Hello World"
      if testPhrase matches /.*World$/
        stdout.println("Pattern match: ends with World")

Common mistakes

E50060 — String has no levenshtein() method. Use the <~> operator, which returns a weighted fuzzy-match cost. See ek9 -h E50060 for details.

Incorrect:

dist1 <- original.levenshtein("sitten")

Correct:

dist1 <- original <~> "sitten"
Other ways to ask this
  • Does EK9 have Levenshtein-style fuzzy matching?
  • How do I find similar strings in EK9?
  • How do I detect typos or approximate matches in EK9?

Coming from another language?

Java: no built-in Levenshtein, requires Apache Commons or custom implementation. Python: no built-in, requires python-Levenshtein or difflib.SequenceMatcher. Rust: no built-in, requires strsim crate. Go: no built-in, requires third-party packages. JavaScript: no built-in, requires libraries. Kotlin: no built-in. EK9: built-in <~> operator returns a weighted Levenshtein-style match cost as an Integer (case change 1, other edits 2, Integer max for no viable match), no libraries needed.

Keywords: levenshtein, similar, compare, typo, string, approximate, match, text, distance, regex, fuzzy