Skip to content

Commit

Permalink
Merge pull request #396 from oclif/sm/distance-algo
Browse files Browse the repository at this point in the history
refactor: algo and test for it
  • Loading branch information
WillieRuemmele committed May 24, 2023
2 parents 2115b32 + b159bf1 commit c08dd44
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 10 deletions.
15 changes: 5 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {color} from '@oclif/color'
import {Hook, toConfiguredId, ux} from '@oclif/core'
import * as Levenshtein from 'fast-levenshtein'

export const closest = (target: string, possibilities: string[]): string =>
possibilities.map(id => ({id, distance: Levenshtein.get(target, id)})).sort((a, b) => a.distance - b.distance)[0]?.id ?? ''

const hook: Hook.CommandNotFound = async function (opts) {
const hiddenCommandIds = new Set(opts.config.commands.filter(c => c.hidden).map(c => c.id))
const commandIDs = [
Expand All @@ -10,15 +13,6 @@ const hook: Hook.CommandNotFound = async function (opts) {
].filter(c => !hiddenCommandIds.has(c))

if (commandIDs.length === 0) return
function closest(cmd: string): string {
// we'll use this array to keep track of which key is the closest to the users entered value.
// keys closer to the index 0 will be a closer guess than keys indexed further from 0
// an entry at 0 would be a direct match, an entry at 1 would be a single character off, etc.
const index: string[] = []
// eslint-disable-next-line no-return-assign
commandIDs.map(id => (index[Levenshtein.get(cmd, id)] = id))
return index.find(item => item !== undefined) ?? ''
}

let binHelp = `${opts.config.bin} help`
const idSplit = opts.id.split(':')
Expand All @@ -27,7 +21,8 @@ const hook: Hook.CommandNotFound = async function (opts) {
binHelp = `${binHelp} ${idSplit[0]}`
}

const suggestion = closest(opts.id)
const suggestion = closest(opts.id, commandIDs)

const readableSuggestion = toConfiguredId(suggestion, this.config)
const originalCmd = toConfiguredId(opts.id, this.config)
this.warn(`${color.yellow(originalCmd)} is not a ${opts.config.bin} command.`)
Expand Down
37 changes: 37 additions & 0 deletions test/closest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {closest} from '../src'
import {expect} from 'chai'

describe('closest', () => {
const possibilities = ['abc', 'def', 'ghi', 'jkl', 'jlk']
it('exact match', () => {
expect(closest('abc', possibilities)).to.equal('abc')
})

it('case mistake', () => {
expect(closest('aBc', possibilities)).to.equal('abc')
})

it('case match one letter', () => {
expect(closest('aZZ', possibilities)).to.equal('abc')
})

it('one letter different mistake', () => {
expect(closest('ggi', possibilities)).to.equal('ghi')
})

it('two letter different mistake', () => {
expect(closest('gki', possibilities)).to.equal('ghi')
})

it('extra letter', () => {
expect(closest('gkui', possibilities)).to.equal('ghi')
})

it('two letter different mistake with close neighbor', () => {
expect(closest('jpp', possibilities)).to.equal('jkl')
})

it('no possibilities gives empty string', () => {
expect(closest('jpp', [])).to.equal('')
})
})

0 comments on commit c08dd44

Please sign in to comment.