did_you_mean 1.3.0 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/CHANGELOG.md +187 -62
- data/Gemfile +2 -0
- data/README.md +40 -36
- data/Rakefile +38 -12
- data/appveyor.yml +25 -0
- data/benchmark/jaro_winkler/memory_usage.rb +4 -4
- data/benchmark/jaro_winkler/speed.rb +14 -6
- data/benchmark/levenshtein/memory_usage.rb +4 -4
- data/benchmark/levenshtein/speed.rb +13 -8
- data/benchmark/require_path_checker.rb +47 -0
- data/benchmark/speed.yml +13 -0
- data/did_you_mean.gemspec +6 -4
- data/{doc → documentation}/CHANGELOG.md.erb +0 -0
- data/{doc → documentation}/changelog_generator.rb +1 -1
- data/documentation/human_typo_api.md +20 -0
- data/documentation/tree_spell_algorithm.md +82 -0
- data/documentation/tree_spell_checker_api.md +24 -0
- data/lib/did_you_mean/core_ext/name_error.rb +11 -6
- data/lib/did_you_mean/experimental.rb +2 -2
- data/lib/did_you_mean/formatter.rb +44 -0
- data/lib/did_you_mean/formatters/plain_formatter.rb +3 -32
- data/lib/did_you_mean/formatters/verbose_formatter.rb +6 -46
- data/lib/did_you_mean/levenshtein.rb +1 -1
- data/lib/did_you_mean/spell_checker.rb +13 -19
- data/lib/did_you_mean/spell_checkers/key_error_checker.rb +8 -2
- data/lib/did_you_mean/spell_checkers/method_name_checker.rb +23 -7
- data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
- data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +4 -1
- data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
- data/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb +20 -0
- data/lib/did_you_mean/spell_checkers/require_path_checker.rb +39 -0
- data/lib/did_you_mean/tree_spell_checker.rb +109 -0
- data/lib/did_you_mean/verbose.rb +2 -4
- data/lib/did_you_mean/version.rb +1 -1
- data/lib/did_you_mean.rb +68 -22
- data/test/core_ext/test_name_error_extension.rb +49 -0
- data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
- data/test/fixtures/book.rb +1 -1
- data/test/fixtures/mini_dir.yml +15 -0
- data/test/fixtures/rspec_dir.yml +112 -0
- data/test/helper.rb +33 -0
- data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +15 -11
- data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
- data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +35 -14
- data/test/spell_checking/test_pattern_key_name_check.rb +20 -0
- data/test/spell_checking/test_require_path_check.rb +32 -0
- data/test/spell_checking/test_uncorrectable_name_check.rb +15 -0
- data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +23 -19
- data/test/test_ractor_compatibility.rb +102 -0
- data/test/{spell_checker_test.rb → test_spell_checker.rb} +3 -2
- data/test/test_tree_spell_checker.rb +170 -0
- data/test/tree_spell/change_word.rb +61 -0
- data/test/tree_spell/human_typo.rb +69 -0
- data/test/tree_spell/test_change_word.rb +38 -0
- data/test/tree_spell/test_explore.rb +128 -0
- data/test/tree_spell/test_human_typo.rb +24 -0
- metadata +60 -67
- data/.ruby-version +0 -1
- data/.travis.yml +0 -15
- data/lib/did_you_mean/experimental/initializer_name_correction.rb +0 -20
- data/lib/did_you_mean/experimental/ivar_name_correction.rb +0 -74
- data/test/core_ext/name_error_extension_test.rb +0 -51
- data/test/experimental/initializer_name_correction_test.rb +0 -15
- data/test/experimental/method_name_checker_test.rb +0 -13
- data/test/spell_checking/uncorrectable_name_check_test.rb +0 -15
- data/test/test_helper.rb +0 -13
- data/test/verbose_formatter_test.rb +0 -23
- data/tmp/.keep +0 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative './helper'
|
2
|
+
|
3
|
+
return if not DidYouMean::TestHelper.ractor_compatible?
|
4
|
+
|
5
|
+
class RactorCompatibilityTest < Test::Unit::TestCase
|
6
|
+
include DidYouMean::TestHelper
|
7
|
+
|
8
|
+
class ::Book; end
|
9
|
+
class FirstNameError < NameError; end
|
10
|
+
|
11
|
+
def test_class_name_suggestion_works_in_ractor
|
12
|
+
error = Ractor.new {
|
13
|
+
begin
|
14
|
+
Boook
|
15
|
+
rescue NameError => e
|
16
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
17
|
+
e
|
18
|
+
end
|
19
|
+
}.take
|
20
|
+
|
21
|
+
assert_correction "Book", error.corrections
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_key_name_suggestion_works_in_ractor
|
25
|
+
error = Ractor.new {
|
26
|
+
begin
|
27
|
+
hash = { "foo" => 1, bar: 2 }
|
28
|
+
|
29
|
+
hash.fetch(:bax)
|
30
|
+
rescue KeyError => e
|
31
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
32
|
+
e
|
33
|
+
end
|
34
|
+
}.take
|
35
|
+
|
36
|
+
assert_correction ":bar", error.corrections
|
37
|
+
assert_match "Did you mean? :bar", error.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_method_name_suggestion_works_in_ractor
|
41
|
+
error = Ractor.new {
|
42
|
+
begin
|
43
|
+
self.to__s
|
44
|
+
rescue NoMethodError => e
|
45
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
46
|
+
e
|
47
|
+
end
|
48
|
+
}.take
|
49
|
+
|
50
|
+
assert_correction :to_s, error.corrections
|
51
|
+
assert_match "Did you mean? to_s", error.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
if defined?(::NoMatchingPatternKeyError)
|
55
|
+
def test_pattern_key_name_suggestion_works_in_ractor
|
56
|
+
error = Ractor.new {
|
57
|
+
begin
|
58
|
+
eval(<<~RUBY, binding, __FILE__, __LINE__)
|
59
|
+
hash = {foo: 1, bar: 2, baz: 3}
|
60
|
+
hash => {fooo:}
|
61
|
+
fooo = 1 # suppress "unused variable: fooo" warning
|
62
|
+
RUBY
|
63
|
+
rescue NoMatchingPatternKeyError => e
|
64
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
65
|
+
e
|
66
|
+
end
|
67
|
+
}.take
|
68
|
+
|
69
|
+
assert_correction ":foo", error.corrections
|
70
|
+
assert_match "Did you mean? :foo", error.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_can_raise_other_name_error_in_ractor
|
75
|
+
error = Ractor.new {
|
76
|
+
begin
|
77
|
+
raise FirstNameError, "Other name error"
|
78
|
+
rescue FirstNameError => e
|
79
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
80
|
+
e
|
81
|
+
end
|
82
|
+
}.take
|
83
|
+
|
84
|
+
assert_not_match(/Did you mean\?/, error.message)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_variable_name_suggestion_works_in_ractor
|
88
|
+
error = Ractor.new {
|
89
|
+
in_ractor = in_ractor = 1
|
90
|
+
|
91
|
+
begin
|
92
|
+
in_reactor
|
93
|
+
rescue NameError => e
|
94
|
+
e.corrections # It is important to call the #corrections method within Ractor.
|
95
|
+
e
|
96
|
+
end
|
97
|
+
}.take
|
98
|
+
|
99
|
+
assert_correction :in_ractor, error.corrections
|
100
|
+
assert_match "Did you mean? in_ractor", error.to_s
|
101
|
+
end
|
102
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
|
1
|
+
require_relative './helper'
|
2
2
|
|
3
|
-
class SpellCheckerTest <
|
3
|
+
class SpellCheckerTest < Test::Unit::TestCase
|
4
4
|
def test_spell_checker_corrects_mistypes
|
5
5
|
assert_spell 'foo', input: 'doo', dictionary: ['foo', 'fork']
|
6
6
|
assert_spell 'email', input: 'meail', dictionary: ['email', 'fail', 'eval']
|
@@ -10,6 +10,7 @@ class SpellCheckerTest < Minitest::Test
|
|
10
10
|
assert_spell 'eval', input: 'veal', dictionary: ['email', 'fail', 'eval']
|
11
11
|
assert_spell 'sub!', input: 'suv!', dictionary: ['sub', 'gsub', 'sub!']
|
12
12
|
assert_spell 'sub', input: 'suv', dictionary: ['sub', 'gsub', 'sub!']
|
13
|
+
assert_spell 'Foo', input: 'FOo', dictionary: ['Foo', 'FOo']
|
13
14
|
|
14
15
|
assert_spell %w(gsub! gsub), input: 'gsuv!', dictionary: %w(sub gsub gsub!)
|
15
16
|
assert_spell %w(sub! sub gsub!), input: 'ssub!', dictionary: %w(sub sub! gsub gsub!)
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require_relative "./helper"
|
6
|
+
|
7
|
+
class TreeSpellCheckerTest < Test::Unit::TestCase
|
8
|
+
MINI_DIRECTORIES = YAML.load_file(File.expand_path("fixtures/mini_dir.yml", __dir__))
|
9
|
+
RSPEC_DIRECTORIES = YAML.load_file(File.expand_path("fixtures/rspec_dir.yml", __dir__))
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@dictionary =
|
13
|
+
%w(
|
14
|
+
spec/models/concerns/vixen_spec.rb
|
15
|
+
spec/models/concerns/abcd_spec.rb
|
16
|
+
spec/models/concerns/vixenus_spec.rb
|
17
|
+
spec/models/concerns/efgh_spec.rb
|
18
|
+
spec/modals/confirms/abcd_spec.rb
|
19
|
+
spec/modals/confirms/efgh_spec.rb
|
20
|
+
spec/models/gafafa_spec.rb
|
21
|
+
spec/models/gfsga_spec.rb
|
22
|
+
spec/controllers/vixen_controller_spec.rb
|
23
|
+
)
|
24
|
+
@test_str = "spek/modeks/confirns/viken_spec.rb"
|
25
|
+
@tree_spell_checker = DidYouMean::TreeSpellChecker.new(dictionary: @dictionary)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_corrupt_root
|
29
|
+
assert_tree_spell "test/verbose_formatter_test.rb",
|
30
|
+
input: "btets/cverbose_formatter_etst.rb suggestions",
|
31
|
+
dictionary: MINI_DIRECTORIES
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_leafless_state
|
35
|
+
assert_tree_spell "spec/modals/confirms/efgh_spec.rb",
|
36
|
+
input: "spec/modals/confirXX/efgh_spec.rb",
|
37
|
+
dictionary: [*@dictionary, "spec/features"]
|
38
|
+
|
39
|
+
assert_tree_spell "spec/features",
|
40
|
+
input: "spec/featuresXX",
|
41
|
+
dictionary: [*@dictionary, "spec/features"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_rake_dictionary
|
45
|
+
assert_tree_spell "parallel:prepare",
|
46
|
+
input: "parallel:preprare",
|
47
|
+
dictionary: %w[parallel:prepare parallel:create parallel:rake parallel:migrate],
|
48
|
+
separator: ":"
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_special_words_mini
|
52
|
+
[
|
53
|
+
%w(test/fixtures/book.rb test/fixture/book.rb),
|
54
|
+
%w(test/edit_distance/jaro_winkler_test.rb test/edit_distace/jaro_winkler_test.rb),
|
55
|
+
%w(test/edit_distance/jaro_winkler_test.rb teste/dit_distane/jaro_winkler_test.rb),
|
56
|
+
%w(test/fixtures/book.rb test/fixturWes/book.rb),
|
57
|
+
%w(test/test_helper.rb tes!t/test_helper.rb),
|
58
|
+
%w(test/fixtures/book.rb test/hfixtures/book.rb),
|
59
|
+
%w(test/edit_distance/jaro_winkler_test.rb test/eidt_distance/jaro_winkler_test.@rb),
|
60
|
+
%w(test/spell_checker_test.rb test/spell_checke@r_test.rb),
|
61
|
+
%w(test/tree_spell_human_typo_test.rb testt/ree_spell_human_typo_test.rb),
|
62
|
+
%w(test/edit_distance/jaro_winkler_test.rb test/edit_distance/jaro_winkler_tuest.rb),
|
63
|
+
].each do |expected, user_input|
|
64
|
+
assert_tree_spell expected, input: user_input, dictionary: MINI_DIRECTORIES
|
65
|
+
end
|
66
|
+
|
67
|
+
[
|
68
|
+
%w(test/spell_checking/variable_name_check_test.rb test/spell_checking/vriabl_ename_check_test.rb),
|
69
|
+
%w(test/spell_checking/key_name_check_test.rb tesit/spell_checking/key_name_choeck_test.rb),
|
70
|
+
].each do |expected, user_input|
|
71
|
+
assert_equal expected, DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct(user_input)[0]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_special_words_rspec
|
76
|
+
[
|
77
|
+
%w(spec/rspec/core/formatters/exception_presenter_spec.rb spec/rspec/core/formatters/eception_presenter_spec.rb),
|
78
|
+
%w(spec/rspec/core/metadata_spec.rb spec/rspec/core/metadata_spe.crb),
|
79
|
+
%w(spec/rspec/core/ordering_spec.rb spec/spec/core/odrering_spec.rb),
|
80
|
+
%w(spec/support/mathn_integration_support.rb spec/support/mathn_itegrtion_support.rb),
|
81
|
+
].each do |expected, user_input|
|
82
|
+
assert_tree_spell expected, input: user_input, dictionary: RSPEC_DIRECTORIES
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_file_in_root
|
87
|
+
assert_tree_spell "test/spell_checker_test.rb", input: "test/spell_checker_test.r", dictionary: MINI_DIRECTORIES
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_no_plausible_states
|
91
|
+
assert_tree_spell [], input: "testspell_checker_test.rb", dictionary: MINI_DIRECTORIES
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_no_plausible_states_with_augmentation
|
95
|
+
assert_tree_spell [], input: "testspell_checker_test.rb", dictionary: MINI_DIRECTORIES
|
96
|
+
|
97
|
+
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct("testspell_checker_test.rb")
|
98
|
+
|
99
|
+
assert_equal suggestions.first, "test/spell_checker_test.rb"
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_no_idea_with_augmentation
|
103
|
+
assert_tree_spell [], input: "test/spell_checking/key_name.rb", dictionary: MINI_DIRECTORIES
|
104
|
+
|
105
|
+
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct("test/spell_checking/key_name.rb")
|
106
|
+
|
107
|
+
assert_equal suggestions.first, "test/spell_checking/key_name_check_test.rb"
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_works_out_suggestions
|
111
|
+
assert_tree_spell %w(spec/models/concerns/vixen_spec.rb spec/models/concerns/vixenus_spec.rb),
|
112
|
+
input: "spek/modeks/confirns/viken_spec.rb",
|
113
|
+
dictionary: %w(spec/models/concerns/vixen_spec.rb spec/models/concerns/vixenus_spec.rb)
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_works_when_input_is_correct
|
117
|
+
assert_tree_spell "spec/models/concerns/vixenus_spec.rb",
|
118
|
+
input: "spec/models/concerns/vixenus_spec.rb",
|
119
|
+
dictionary: @dictionary
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_find_out_leaves_in_a_path
|
123
|
+
names = @tree_spell_checker.find_leaves("spec/modals/confirms")
|
124
|
+
|
125
|
+
assert_equal %w[abcd_spec.rb efgh_spec.rb], names
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_works_out_nodes
|
129
|
+
exp_paths = ["spec/models/concerns",
|
130
|
+
"spec/models/confirms",
|
131
|
+
"spec/modals/concerns",
|
132
|
+
"spec/modals/confirms",
|
133
|
+
"spec/controllers/concerns",
|
134
|
+
"spec/controllers/confirms"]
|
135
|
+
|
136
|
+
states = @tree_spell_checker.dimensions
|
137
|
+
nodes = states[0].product(*states[1..-1])
|
138
|
+
paths = @tree_spell_checker.possible_paths(nodes)
|
139
|
+
|
140
|
+
assert_equal paths, exp_paths
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_works_out_state_space
|
144
|
+
suggestions = @tree_spell_checker.plausible_dimensions(@test_str)
|
145
|
+
|
146
|
+
assert_equal [["spec"], %w[models modals], %w[confirms concerns]], suggestions
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_parses_dictionary
|
150
|
+
states = @tree_spell_checker.dimensions
|
151
|
+
|
152
|
+
assert_equal [["spec"], %w[models modals controllers], %w[concerns confirms]], states
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_parses_elementary_dictionary
|
156
|
+
dimensions = DidYouMean::TreeSpellChecker
|
157
|
+
.new(dictionary: %w(spec/models/user_spec.rb spec/services/account_spec.rb))
|
158
|
+
.dimensions
|
159
|
+
|
160
|
+
assert_equal [["spec"], %w[models services]], dimensions
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def assert_tree_spell(expected, input:, dictionary:, separator: "/")
|
166
|
+
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: dictionary, separator: separator).correct(input)
|
167
|
+
|
168
|
+
assert_equal Array(expected), suggestions, "Expected to suggest #{expected}, but got #{suggestions.inspect}"
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module TreeSpell
|
2
|
+
# Changes a word with one of four actions:
|
3
|
+
# insertion, substitution, deletion and transposition.
|
4
|
+
class ChangeWord
|
5
|
+
# initialize with input string
|
6
|
+
def initialize(input)
|
7
|
+
@input = input
|
8
|
+
@len = input.length
|
9
|
+
end
|
10
|
+
|
11
|
+
# insert char after index of i_place
|
12
|
+
def insertion(i_place, char)
|
13
|
+
@word = input.dup
|
14
|
+
return char + word if i_place == 0
|
15
|
+
return word + char if i_place == len - 1
|
16
|
+
word.insert(i_place + 1, char)
|
17
|
+
end
|
18
|
+
|
19
|
+
# substitute char at index of i_place
|
20
|
+
def substitution(i_place, char)
|
21
|
+
@word = input.dup
|
22
|
+
word[i_place] = char
|
23
|
+
word
|
24
|
+
end
|
25
|
+
|
26
|
+
# delete character at index of i_place
|
27
|
+
def deletion(i_place)
|
28
|
+
@word = input.dup
|
29
|
+
word.slice!(i_place)
|
30
|
+
word
|
31
|
+
end
|
32
|
+
|
33
|
+
# transpose char at i_place with char at i_place + direction
|
34
|
+
# if i_place + direction is out of bounds just swap in other direction
|
35
|
+
def transposition(i_place, direction)
|
36
|
+
@word = input.dup
|
37
|
+
w = word.dup
|
38
|
+
return swap_first_two(w) if i_place + direction < 0
|
39
|
+
return swap_last_two(w) if i_place + direction >= len
|
40
|
+
swap_two(w, i_place, direction)
|
41
|
+
w
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_accessor :word, :input, :len
|
47
|
+
|
48
|
+
def swap_first_two(w)
|
49
|
+
w[1] + w[0] + word[2..-1]
|
50
|
+
end
|
51
|
+
|
52
|
+
def swap_last_two(w)
|
53
|
+
w[0...(len - 2)] + word[len - 1] + word[len - 2]
|
54
|
+
end
|
55
|
+
|
56
|
+
def swap_two(w, i_place, direction)
|
57
|
+
w[i_place] = word[i_place + direction]
|
58
|
+
w[i_place + direction] = word[i_place]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# module for classes needed to test TreeSpellChecker
|
2
|
+
module TreeSpell
|
3
|
+
require_relative 'change_word'
|
4
|
+
# Simulate an error prone human typist
|
5
|
+
# see doc/human_typo_api.md for the api description
|
6
|
+
class HumanTypo
|
7
|
+
POPULAR_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?<>,.!`+=-_":;@#$%^&*()'.split("").freeze
|
8
|
+
ACTION_TYPES = %i(insert transpose delete substitute).freeze
|
9
|
+
|
10
|
+
def initialize(input, lambda: 0.05)
|
11
|
+
@input = input
|
12
|
+
check_input
|
13
|
+
@len = input.length
|
14
|
+
@lambda = lambda
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
@word = input.dup
|
19
|
+
i_place = initialize_i_place
|
20
|
+
loop do
|
21
|
+
action = ACTION_TYPES.sample
|
22
|
+
@word = make_change action, i_place
|
23
|
+
@len = word.length
|
24
|
+
i_place += exponential
|
25
|
+
break if i_place >= len
|
26
|
+
end
|
27
|
+
word
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_accessor :input, :word, :len, :lambda
|
33
|
+
|
34
|
+
def initialize_i_place
|
35
|
+
i_place = nil
|
36
|
+
loop do
|
37
|
+
i_place = exponential
|
38
|
+
break if i_place < len
|
39
|
+
end
|
40
|
+
i_place
|
41
|
+
end
|
42
|
+
|
43
|
+
def exponential
|
44
|
+
(rand / (lambda / 2)).to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def make_change(action, i_place)
|
48
|
+
cw = ChangeWord.new(word)
|
49
|
+
case action
|
50
|
+
when :delete
|
51
|
+
cw.deletion(i_place)
|
52
|
+
when :insert
|
53
|
+
cw.insertion(i_place, POPULAR_CHARS.sample)
|
54
|
+
when :substitute
|
55
|
+
cw.substitution(i_place, POPULAR_CHARS.sample)
|
56
|
+
when :transpose
|
57
|
+
cw.transposition(i_place, rand >= 0.5 ? +1 : -1)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_input
|
62
|
+
fail check_input_message if input.nil? || input.length < 5
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_input_message
|
66
|
+
"input length must be greater than 5 characters: #{input}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require_relative 'change_word'
|
3
|
+
|
4
|
+
class ChangeWordTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@input = 'spec/services/anything_spec'
|
7
|
+
@cw = TreeSpell::ChangeWord.new(@input)
|
8
|
+
@len = @input.length
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_deletion
|
12
|
+
assert_match @cw.deletion(5), 'spec/ervices/anything_spec'
|
13
|
+
assert_match @cw.deletion(@len - 1), 'spec/services/anything_spe'
|
14
|
+
assert_match @cw.deletion(0), 'pec/services/anything_spec'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_substitution
|
18
|
+
assert_match @cw.substitution(5, '$'), 'spec/$ervices/anything_spec'
|
19
|
+
assert_match @cw.substitution(@len - 1, '$'), 'spec/services/anything_spe$'
|
20
|
+
assert_match @cw.substitution(0, '$'), '$pec/services/anything_spec'
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_insertion
|
24
|
+
assert_match @cw.insertion(7, 'X'), 'spec/serXvices/anything_spec'
|
25
|
+
assert_match @cw.insertion(0, 'X'), 'Xspec/services/anything_spec'
|
26
|
+
assert_match @cw.insertion(@len - 1, 'X'), 'spec/services/anything_specX'
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_transposition
|
30
|
+
n = @input.length
|
31
|
+
assert_match @cw.transposition(0, -1), 'psec/services/anything_spec'
|
32
|
+
assert_match @cw.transposition(n - 1, +1), 'spec/services/anything_spce'
|
33
|
+
assert_match @cw.transposition(4, +1), 'specs/ervices/anything_spec'
|
34
|
+
assert_match @cw.transposition(4, -1), 'spe/cservices/anything_spec'
|
35
|
+
assert_match @cw.transposition(21, -1), 'spec/services/anythign_spec'
|
36
|
+
assert_match @cw.transposition(21, +1), 'spec/services/anythin_gspec'
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require_relative '../helper'
|
5
|
+
require_relative 'human_typo'
|
6
|
+
|
7
|
+
# statistical tests on tree_spell algorithms
|
8
|
+
class ExploreTest < Test::Unit::TestCase
|
9
|
+
MINI_DIRECTORIES = YAML.load_file(File.expand_path('../fixtures/mini_dir.yml', __dir__))
|
10
|
+
RSPEC_DIRECTORIES = YAML.load_file(File.expand_path('../fixtures/rspec_dir.yml', __dir__))
|
11
|
+
|
12
|
+
def test_checkers_with_many_typos_on_mini
|
13
|
+
n_repeat = 10_000
|
14
|
+
many_typos n_repeat, MINI_DIRECTORIES, 'Minitest'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_checkers_with_many_typos_on_rspec
|
18
|
+
n_repeat = 10_000
|
19
|
+
many_typos n_repeat, RSPEC_DIRECTORIES, 'Rspec'
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_human_typo
|
23
|
+
n_repeat = 10_000
|
24
|
+
total_changes = 0
|
25
|
+
word = 'any_string_that_is_40_characters_long_sp'
|
26
|
+
n_repeat.times do
|
27
|
+
word_error = TreeSpell::HumanTypo.new(word).call
|
28
|
+
total_changes += DidYouMean::Levenshtein.distance(word, word_error)
|
29
|
+
end
|
30
|
+
mean_changes = (total_changes.to_f / n_repeat).round(2)
|
31
|
+
puts ''
|
32
|
+
puts "HumanTypo mean_changes: #{mean_changes} with n_repeat: #{n_repeat}"
|
33
|
+
puts 'Expected mean_changes: 2.1 with n_repeat: 10000, plus/minus 0.03'
|
34
|
+
puts ''
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_execution_speed
|
38
|
+
n_repeat = 1_000
|
39
|
+
puts ''
|
40
|
+
puts 'Testing execution time of Standard'
|
41
|
+
measure_execution_speed(n_repeat) do |files, error|
|
42
|
+
DidYouMean::SpellChecker.new(dictionary: files).correct error
|
43
|
+
end
|
44
|
+
puts ''
|
45
|
+
puts 'Testing execution time of Tree'
|
46
|
+
measure_execution_speed(n_repeat) do |files, error|
|
47
|
+
DidYouMean::TreeSpellChecker.new(dictionary: files).correct error
|
48
|
+
end
|
49
|
+
puts ''
|
50
|
+
puts 'Testing execution time of Augmented Tree'
|
51
|
+
measure_execution_speed(n_repeat) do |files, error|
|
52
|
+
DidYouMean::TreeSpellChecker.new(dictionary: files, augment: true).correct error
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def measure_execution_speed(n_repeat, &block)
|
59
|
+
len = RSPEC_DIRECTORIES.length
|
60
|
+
start_time = Time.now
|
61
|
+
n_repeat.times do
|
62
|
+
word = RSPEC_DIRECTORIES[rand len]
|
63
|
+
word_error = TreeSpell::HumanTypo.new(word).call
|
64
|
+
block.call(RSPEC_DIRECTORIES, word_error)
|
65
|
+
end
|
66
|
+
time_ms = (Time.now - start_time).to_f * 1000 / n_repeat
|
67
|
+
puts "Average time (ms): #{time_ms.round(1)}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def many_typos(n_repeat, files, title)
|
71
|
+
first_times = [0, 0, 0]
|
72
|
+
total_suggestions = [0, 0, 0]
|
73
|
+
total_failures = [0, 0, 0]
|
74
|
+
len = files.length
|
75
|
+
n_repeat.times do
|
76
|
+
word = files[rand len]
|
77
|
+
word_error = TreeSpell::HumanTypo.new(word).call
|
78
|
+
suggestions_a = group_suggestions word_error, files
|
79
|
+
check_first_is_right word, suggestions_a, first_times
|
80
|
+
check_no_suggestions suggestions_a, total_suggestions
|
81
|
+
check_for_failure word, suggestions_a, total_failures
|
82
|
+
end
|
83
|
+
print_results first_times, total_suggestions, total_failures, n_repeat, title
|
84
|
+
end
|
85
|
+
|
86
|
+
def group_suggestions(word_error, files)
|
87
|
+
a0 = DidYouMean::TreeSpellChecker.new(dictionary: files).correct word_error
|
88
|
+
a1 = ::DidYouMean::SpellChecker.new(dictionary: files).correct word_error
|
89
|
+
a2 = a0.empty? ? a1 : a0
|
90
|
+
[a0, a1, a2]
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_for_failure(word, suggestions_a, total_failures)
|
94
|
+
suggestions_a.each_with_index.map do |a, i|
|
95
|
+
total_failures[i] += 1 unless a.include? word
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_first_is_right(word, suggestions_a, first_times)
|
100
|
+
suggestions_a.each_with_index.map do |a, i|
|
101
|
+
first_times[i] += 1 if word == a.first
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def check_no_suggestions(suggestions_a, total_suggestions)
|
106
|
+
suggestions_a.each_with_index.map do |a, i|
|
107
|
+
total_suggestions[i] += a.length
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def print_results(first_times, total_suggestions, total_failures, n_repeat, title)
|
112
|
+
algorithms = ['Tree ', 'Standard ', 'Augmented']
|
113
|
+
print_header title
|
114
|
+
(0..2).each do |i|
|
115
|
+
ft = (first_times[i].to_f / n_repeat * 100).round(1)
|
116
|
+
mns = (total_suggestions[i].to_f / (n_repeat - total_failures[i])).round(1)
|
117
|
+
f = (total_failures[i].to_f / n_repeat * 100).round(1)
|
118
|
+
puts " #{algorithms[i]} #{' ' * 7} #{ft} #{' ' * 14} #{mns} #{' ' * 15} #{f} #{' ' * 16}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def print_header(title)
|
123
|
+
puts "#{' ' * 30} #{title} Summary #{' ' * 31}"
|
124
|
+
puts '-' * 80
|
125
|
+
puts " Method | First Time (\%) Mean Suggestions Failures (\%) #{' ' * 13}"
|
126
|
+
puts '-' * 80
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require_relative 'human_typo'
|
3
|
+
|
4
|
+
class HumanTypoTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@input = 'spec/services/anything_spec'
|
7
|
+
@sh = TreeSpell::HumanTypo.new(@input, lambda: 0.05)
|
8
|
+
@len = @input.length
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_changes
|
12
|
+
# srand seed ensures all four actions are called
|
13
|
+
srand 247_696_449
|
14
|
+
sh = TreeSpell::HumanTypo.new(@input, lambda: 0.20)
|
15
|
+
word_error = sh.call
|
16
|
+
assert_equal word_error, 'spec/suervcieq/anythin_gpec'
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_check_input
|
20
|
+
assert_raise(RuntimeError, "input length must be greater than 5 characters: tiny") do
|
21
|
+
TreeSpell::HumanTypo.new('tiny')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|