did_you_mean 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +48 -0
  3. data/CHANGELOG.md +75 -75
  4. data/Gemfile +2 -1
  5. data/README.md +2 -32
  6. data/Rakefile +4 -5
  7. data/appveyor.yml +25 -0
  8. data/did_you_mean.gemspec +6 -4
  9. data/documentation/CHANGELOG.md.erb +8 -0
  10. data/documentation/changelog_generator.rb +34 -0
  11. data/documentation/human_typo_api.md +20 -0
  12. data/documentation/tree_spell_algorithm.md +82 -0
  13. data/documentation/tree_spell_checker_api.md +24 -0
  14. data/lib/did_you_mean.rb +17 -16
  15. data/lib/did_you_mean/experimental.rb +2 -2
  16. data/lib/did_you_mean/experimental/initializer_name_correction.rb +1 -1
  17. data/lib/did_you_mean/experimental/ivar_name_correction.rb +3 -1
  18. data/lib/did_you_mean/levenshtein.rb +1 -1
  19. data/lib/did_you_mean/spell_checker.rb +7 -7
  20. data/lib/did_you_mean/spell_checkers/key_error_checker.rb +8 -2
  21. data/lib/did_you_mean/spell_checkers/method_name_checker.rb +14 -6
  22. data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
  23. data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
  24. data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +1 -1
  25. data/lib/did_you_mean/tree_spell_checker.rb +137 -0
  26. data/lib/did_you_mean/verbose.rb +2 -2
  27. data/lib/did_you_mean/version.rb +1 -1
  28. data/test/core_ext/test_name_error_extension.rb +48 -0
  29. data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
  30. data/test/fixtures/mini_dir.yml +15 -0
  31. data/test/fixtures/rspec_dir.yml +112 -0
  32. data/test/helper.rb +29 -0
  33. data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +12 -10
  34. data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
  35. data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +17 -15
  36. data/test/spell_checking/{uncorrectable_name_check_test.rb → test_uncorrectable_name_check.rb} +3 -3
  37. data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +18 -16
  38. data/test/{spell_checker_test.rb → test_spell_checker.rb} +2 -2
  39. data/test/test_tree_spell_checker.rb +173 -0
  40. data/test/test_verbose_formatter.rb +21 -0
  41. data/test/tree_spell/change_word.rb +61 -0
  42. data/test/tree_spell/human_typo.rb +89 -0
  43. data/test/tree_spell/test_change_word.rb +38 -0
  44. data/test/tree_spell/test_explore.rb +128 -0
  45. data/test/tree_spell/test_human_typo.rb +24 -0
  46. metadata +47 -58
  47. data/.travis.yml +0 -23
  48. data/test/core_ext/name_error_extension_test.rb +0 -51
  49. data/test/experimental/initializer_name_correction_test.rb +0 -15
  50. data/test/experimental/method_name_checker_test.rb +0 -13
  51. data/test/test_helper.rb +0 -13
  52. data/test/verbose_formatter_test.rb +0 -22
@@ -0,0 +1,21 @@
1
+ require_relative './helper'
2
+
3
+ class VerboseFormatterTest < Test::Unit::TestCase
4
+ def setup
5
+ require_relative File.join(DidYouMean::TestHelper.root, 'verbose')
6
+ end
7
+
8
+ def teardown
9
+ DidYouMean.formatter = DidYouMean::PlainFormatter.new
10
+ end
11
+
12
+ def test_message
13
+ @error = assert_raise(NoMethodError){ 1.zeor? }
14
+
15
+ assert_match <<~MESSAGE.strip, @error.message
16
+ undefined method `zeor?' for 1:Integer
17
+
18
+ Did you mean? zero?
19
+ MESSAGE
20
+ end
21
+ 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,89 @@
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
+ def initialize(input, lambda: 0.05)
8
+ @input = input
9
+ check_input
10
+ @len = input.length
11
+ @lambda = lambda
12
+ end
13
+
14
+ def call
15
+ @word = input.dup
16
+ i_place = initialize_i_place
17
+ loop do
18
+ action = action_type
19
+ @word = make_change action, i_place
20
+ @len = word.length
21
+ i_place += exponential
22
+ break if i_place >= len
23
+ end
24
+ word
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :input, :word, :len, :lambda
30
+
31
+ def initialize_i_place
32
+ i_place = nil
33
+ loop do
34
+ i_place = exponential
35
+ break if i_place < len
36
+ end
37
+ i_place
38
+ end
39
+
40
+ def exponential
41
+ (rand / (lambda / 2)).to_i
42
+ end
43
+
44
+ def rand_char
45
+ popular_chars = alphabetic_characters + special_characters
46
+ n = popular_chars.length
47
+ popular_chars[rand(n)]
48
+ end
49
+
50
+ def alphabetic_characters
51
+ ('a'..'z').to_a.join + ('A'..'Z').to_a.join
52
+ end
53
+
54
+ def special_characters
55
+ '?<>,.!`+=-_":;@#$%^&*()'
56
+ end
57
+
58
+ def toss
59
+ return +1 if rand >= 0.5
60
+ -1
61
+ end
62
+
63
+ def action_type
64
+ [:insert, :transpose, :delete, :substitute][rand(4)]
65
+ end
66
+
67
+ def make_change(action, i_place)
68
+ cw = ChangeWord.new(word)
69
+ case action
70
+ when :delete
71
+ cw.deletion(i_place)
72
+ when :insert
73
+ cw.insertion(i_place, rand_char)
74
+ when :substitute
75
+ cw.substitution(i_place, rand_char)
76
+ when :transpose
77
+ cw.transposition(i_place, toss)
78
+ end
79
+ end
80
+
81
+ def check_input
82
+ fail check_input_message if input.nil? || input.length < 5
83
+ end
84
+
85
+ def check_input_message
86
+ "input length must be greater than 5 characters: #{input}"
87
+ end
88
+ end
89
+ 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_deleletion
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
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: did_you_mean
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuki Nishijima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-29 00:00:00.000000000 Z
11
+ date: 2019-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -38,20 +24,6 @@ dependencies:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: minitest
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
27
  description: The gem that has been saving people from typos since 2014.
56
28
  email:
57
29
  - mail@yukinishijima.net
@@ -59,13 +31,14 @@ executables: []
59
31
  extensions: []
60
32
  extra_rdoc_files: []
61
33
  files:
34
+ - ".github/workflows/ruby.yml"
62
35
  - ".gitignore"
63
- - ".travis.yml"
64
36
  - CHANGELOG.md
65
37
  - Gemfile
66
38
  - LICENSE.txt
67
39
  - README.md
68
40
  - Rakefile
41
+ - appveyor.yml
69
42
  - benchmark/jaro_winkler/memory_usage.rb
70
43
  - benchmark/jaro_winkler/speed.rb
71
44
  - benchmark/levenshtein/memory_usage.rb
@@ -73,8 +46,11 @@ files:
73
46
  - benchmark/memory_usage.rb
74
47
  - benchmark/speed.yml
75
48
  - did_you_mean.gemspec
76
- - doc/CHANGELOG.md.erb
77
- - doc/changelog_generator.rb
49
+ - documentation/CHANGELOG.md.erb
50
+ - documentation/changelog_generator.rb
51
+ - documentation/human_typo_api.md
52
+ - documentation/tree_spell_algorithm.md
53
+ - documentation/tree_spell_checker_api.md
78
54
  - lib/did_you_mean.rb
79
55
  - lib/did_you_mean/core_ext/name_error.rb
80
56
  - lib/did_you_mean/experimental.rb
@@ -91,22 +67,29 @@ files:
91
67
  - lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb
92
68
  - lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb
93
69
  - lib/did_you_mean/spell_checkers/null_checker.rb
70
+ - lib/did_you_mean/tree_spell_checker.rb
94
71
  - lib/did_you_mean/verbose.rb
95
72
  - lib/did_you_mean/version.rb
96
- - test/core_ext/name_error_extension_test.rb
97
- - test/edit_distance/jaro_winkler_test.rb
98
- - test/experimental/initializer_name_correction_test.rb
99
- - test/experimental/method_name_checker_test.rb
73
+ - test/core_ext/test_name_error_extension.rb
74
+ - test/edit_distance/test_jaro_winkler.rb
100
75
  - test/fixtures/book.rb
101
- - test/spell_checker_test.rb
102
- - test/spell_checking/class_name_check_test.rb
103
- - test/spell_checking/key_name_check_test.rb
104
- - test/spell_checking/method_name_check_test.rb
105
- - test/spell_checking/uncorrectable_name_check_test.rb
106
- - test/spell_checking/variable_name_check_test.rb
107
- - test/test_helper.rb
108
- - test/verbose_formatter_test.rb
109
- homepage: https://github.com/yuki24/did_you_mean
76
+ - test/fixtures/mini_dir.yml
77
+ - test/fixtures/rspec_dir.yml
78
+ - test/helper.rb
79
+ - test/spell_checking/test_class_name_check.rb
80
+ - test/spell_checking/test_key_name_check.rb
81
+ - test/spell_checking/test_method_name_check.rb
82
+ - test/spell_checking/test_uncorrectable_name_check.rb
83
+ - test/spell_checking/test_variable_name_check.rb
84
+ - test/test_spell_checker.rb
85
+ - test/test_tree_spell_checker.rb
86
+ - test/test_verbose_formatter.rb
87
+ - test/tree_spell/change_word.rb
88
+ - test/tree_spell/human_typo.rb
89
+ - test/tree_spell/test_change_word.rb
90
+ - test/tree_spell/test_explore.rb
91
+ - test/tree_spell/test_human_typo.rb
92
+ homepage: https://github.com/ruby/did_you_mean
110
93
  licenses:
111
94
  - MIT
112
95
  metadata: {}
@@ -130,16 +113,22 @@ signing_key:
130
113
  specification_version: 4
131
114
  summary: '"Did you mean?" experience in Ruby'
132
115
  test_files:
133
- - test/core_ext/name_error_extension_test.rb
134
- - test/edit_distance/jaro_winkler_test.rb
135
- - test/experimental/initializer_name_correction_test.rb
136
- - test/experimental/method_name_checker_test.rb
116
+ - test/core_ext/test_name_error_extension.rb
117
+ - test/edit_distance/test_jaro_winkler.rb
137
118
  - test/fixtures/book.rb
138
- - test/spell_checker_test.rb
139
- - test/spell_checking/class_name_check_test.rb
140
- - test/spell_checking/key_name_check_test.rb
141
- - test/spell_checking/method_name_check_test.rb
142
- - test/spell_checking/uncorrectable_name_check_test.rb
143
- - test/spell_checking/variable_name_check_test.rb
144
- - test/test_helper.rb
145
- - test/verbose_formatter_test.rb
119
+ - test/fixtures/mini_dir.yml
120
+ - test/fixtures/rspec_dir.yml
121
+ - test/helper.rb
122
+ - test/spell_checking/test_class_name_check.rb
123
+ - test/spell_checking/test_key_name_check.rb
124
+ - test/spell_checking/test_method_name_check.rb
125
+ - test/spell_checking/test_uncorrectable_name_check.rb
126
+ - test/spell_checking/test_variable_name_check.rb
127
+ - test/test_spell_checker.rb
128
+ - test/test_tree_spell_checker.rb
129
+ - test/test_verbose_formatter.rb
130
+ - test/tree_spell/change_word.rb
131
+ - test/tree_spell/human_typo.rb
132
+ - test/tree_spell/test_change_word.rb
133
+ - test/tree_spell/test_explore.rb
134
+ - test/tree_spell/test_human_typo.rb