forked from Azure/bicep
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SpellChecker.cs
144 lines (119 loc) · 4.87 KB
/
SpellChecker.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
namespace Bicep.Core.Text
{
public static class SpellChecker
{
public static string? GetSpellingSuggestion(string name, IEnumerable<string> candidates)
{
// 0.34 allows 1 insertion/deletion/substitution for every 3 characters.
int maxLengthDifference = Min((int)Math.Floor(name.Length * 0.34), 2);
int maxDistance = (int)Math.Floor(name.Length * 0.34) + 1;
string? bestCandidate = null;
bool checkCaseInsensitiveEqualityOnly = false;
string nameUpperCase = name.ToUpperInvariant();
foreach (string candidate in candidates)
{
if (string.IsNullOrEmpty(candidate) || Math.Abs(name.Length - candidate.Length) > maxLengthDifference)
{
continue;
}
string candidateUpperCase = candidate.ToUpperInvariant();
if (nameUpperCase.Equals(candidateUpperCase))
{
return candidate;
}
if (checkCaseInsensitiveEqualityOnly)
{
continue;
}
if (candidate.Length < 3)
{
// Don't bother for such a short candidate name.
continue;
}
int distance = GetLevenshteinDistanceWithMax(nameUpperCase, candidateUpperCase, maxDistance - 1);
if (distance < 0)
{
continue;
}
if (distance < 2)
{
/*
* There won't be any better result with a smaller Levenshtein distance,
* so check case-insensitive equality for the rest of the candidates.
*/
checkCaseInsensitiveEqualityOnly = true;
bestCandidate = candidate;
}
else
{
Debug.Assert(distance < maxDistance);
maxDistance = distance;
bestCandidate = candidate;
}
}
return bestCandidate;
}
private static int GetLevenshteinDistanceWithMax(string first, string second, int maxDistance)
{
if (Math.Abs(first.Length - second.Length) > maxDistance)
{
return -1;
}
/*
* Lev(i, j): Levenshtein distance between first.Substring(0, i) and second.Substring(0, j).
* Using two 1D arrays instead of one 2d array to reduce space complexity.
*/
var previous = new int[second.Length + 1];
var current = new int[second.Length + 1];
var boundary = maxDistance + 1;
// Initialize Lev(0, j).
for (int j = 0; j <= second.Length; j++)
{
previous[j] = j;
}
// Start DP to fill Lev(i, j).
for (int i = 1; i <= first.Length; i++)
{
int minJ = i > maxDistance ? i - maxDistance : 1;
int maxJ = second.Length > maxDistance + i ? maxDistance + i : second.Length;
int minDistance = i;
// Lev(i, 0).
current[0] = i;
// Lev(i, 1) to Lev(i, minJ - 1).
for (int j = 1; j < minJ; j++)
{
current[j] = boundary;
}
// Lev(i, minJ) to Lev(i, maxJ).
for (int j = minJ; j <= maxJ; j++)
{
// See: https://en.wikipedia.org/wiki/Levenshtein_distance#Definition
current[j] = first[i - 1] == second[j - 1]
? previous[j - 1]
: 1 + Min(previous[j], current[j - 1], previous[j - 1]);
minDistance = Min(minDistance, current[j]);
}
// Lev(i, maxJ + 1) to Lev(i, second.Length).
for (int j = maxJ + 1; j <= second.Length; j++)
{
current[j] = boundary;
}
if (minDistance > maxDistance)
{
// Short circuit since we will not get any better result later for longer strings.
return -1;
}
(previous, current) = (current, previous);
}
/*
* When the DP completes, previous stores
* Lev(first.Length, 0), Lev(first.Length, 1), ..., Lev(first.Length, second.Length).
*/
return previous[second.Length] > maxDistance ? -1 : previous[second.Length];
}
private static int Min(params int[] values) => values.Min();
}
}