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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/CHANGELOG.md +187 -62
  4. data/Gemfile +2 -0
  5. data/README.md +40 -36
  6. data/Rakefile +38 -12
  7. data/appveyor.yml +25 -0
  8. data/benchmark/jaro_winkler/memory_usage.rb +4 -4
  9. data/benchmark/jaro_winkler/speed.rb +14 -6
  10. data/benchmark/levenshtein/memory_usage.rb +4 -4
  11. data/benchmark/levenshtein/speed.rb +13 -8
  12. data/benchmark/require_path_checker.rb +47 -0
  13. data/benchmark/speed.yml +13 -0
  14. data/did_you_mean.gemspec +6 -4
  15. data/{doc → documentation}/CHANGELOG.md.erb +0 -0
  16. data/{doc → documentation}/changelog_generator.rb +1 -1
  17. data/documentation/human_typo_api.md +20 -0
  18. data/documentation/tree_spell_algorithm.md +82 -0
  19. data/documentation/tree_spell_checker_api.md +24 -0
  20. data/lib/did_you_mean/core_ext/name_error.rb +11 -6
  21. data/lib/did_you_mean/experimental.rb +2 -2
  22. data/lib/did_you_mean/formatter.rb +44 -0
  23. data/lib/did_you_mean/formatters/plain_formatter.rb +3 -32
  24. data/lib/did_you_mean/formatters/verbose_formatter.rb +6 -46
  25. data/lib/did_you_mean/levenshtein.rb +1 -1
  26. data/lib/did_you_mean/spell_checker.rb +13 -19
  27. data/lib/did_you_mean/spell_checkers/key_error_checker.rb +8 -2
  28. data/lib/did_you_mean/spell_checkers/method_name_checker.rb +23 -7
  29. data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
  30. data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +4 -1
  31. data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
  32. data/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb +20 -0
  33. data/lib/did_you_mean/spell_checkers/require_path_checker.rb +39 -0
  34. data/lib/did_you_mean/tree_spell_checker.rb +109 -0
  35. data/lib/did_you_mean/verbose.rb +2 -4
  36. data/lib/did_you_mean/version.rb +1 -1
  37. data/lib/did_you_mean.rb +68 -22
  38. data/test/core_ext/test_name_error_extension.rb +49 -0
  39. data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
  40. data/test/fixtures/book.rb +1 -1
  41. data/test/fixtures/mini_dir.yml +15 -0
  42. data/test/fixtures/rspec_dir.yml +112 -0
  43. data/test/helper.rb +33 -0
  44. data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +15 -11
  45. data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
  46. data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +35 -14
  47. data/test/spell_checking/test_pattern_key_name_check.rb +20 -0
  48. data/test/spell_checking/test_require_path_check.rb +32 -0
  49. data/test/spell_checking/test_uncorrectable_name_check.rb +15 -0
  50. data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +23 -19
  51. data/test/test_ractor_compatibility.rb +102 -0
  52. data/test/{spell_checker_test.rb → test_spell_checker.rb} +3 -2
  53. data/test/test_tree_spell_checker.rb +170 -0
  54. data/test/tree_spell/change_word.rb +61 -0
  55. data/test/tree_spell/human_typo.rb +69 -0
  56. data/test/tree_spell/test_change_word.rb +38 -0
  57. data/test/tree_spell/test_explore.rb +128 -0
  58. data/test/tree_spell/test_human_typo.rb +24 -0
  59. metadata +60 -67
  60. data/.ruby-version +0 -1
  61. data/.travis.yml +0 -15
  62. data/lib/did_you_mean/experimental/initializer_name_correction.rb +0 -20
  63. data/lib/did_you_mean/experimental/ivar_name_correction.rb +0 -74
  64. data/test/core_ext/name_error_extension_test.rb +0 -51
  65. data/test/experimental/initializer_name_correction_test.rb +0 -15
  66. data/test/experimental/method_name_checker_test.rb +0 -13
  67. data/test/spell_checking/uncorrectable_name_check_test.rb +0 -15
  68. data/test/test_helper.rb +0 -13
  69. data/test/verbose_formatter_test.rb +0 -23
  70. 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
- require 'test_helper'
1
+ require_relative './helper'
2
2
 
3
- class SpellCheckerTest < Minitest::Test
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