did_you_mean 1.2.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/CHANGELOG.md +172 -57
  4. data/Gemfile +2 -0
  5. data/README.md +79 -28
  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/documentation/CHANGELOG.md.erb +8 -0
  16. data/documentation/changelog_generator.rb +34 -0
  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.rb +95 -52
  21. data/lib/did_you_mean/core_ext/name_error.rb +4 -6
  22. data/lib/did_you_mean/experimental.rb +2 -2
  23. data/lib/did_you_mean/formatters/plain_formatter.rb +24 -0
  24. data/lib/did_you_mean/formatters/verbose_formatter.rb +36 -0
  25. data/lib/did_you_mean/levenshtein.rb +1 -1
  26. data/lib/did_you_mean/spell_checker.rb +8 -14
  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 +48 -6
  29. data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
  30. data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
  31. data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +56 -4
  32. data/lib/did_you_mean/spell_checkers/require_path_checker.rb +35 -0
  33. data/lib/did_you_mean/tree_spell_checker.rb +109 -0
  34. data/lib/did_you_mean/verbose.rb +2 -2
  35. data/lib/did_you_mean/version.rb +1 -1
  36. data/test/core_ext/test_name_error_extension.rb +48 -0
  37. data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
  38. data/test/fixtures/book.rb +1 -1
  39. data/test/fixtures/mini_dir.yml +15 -0
  40. data/test/fixtures/rspec_dir.yml +112 -0
  41. data/test/helper.rb +29 -0
  42. data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +15 -11
  43. data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
  44. data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +41 -13
  45. data/test/spell_checking/test_require_path_check.rb +32 -0
  46. data/test/spell_checking/{uncorrectable_name_check_test.rb → test_uncorrectable_name_check.rb} +3 -3
  47. data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +36 -18
  48. data/test/{spell_checker_test.rb → test_spell_checker.rb} +2 -2
  49. data/test/test_tree_spell_checker.rb +170 -0
  50. data/test/test_verbose_formatter.rb +23 -0
  51. data/test/tree_spell/change_word.rb +61 -0
  52. data/test/tree_spell/human_typo.rb +69 -0
  53. data/test/tree_spell/test_change_word.rb +38 -0
  54. data/test/tree_spell/test_explore.rb +128 -0
  55. data/test/tree_spell/test_human_typo.rb +24 -0
  56. metadata +53 -66
  57. data/.ruby-version +0 -1
  58. data/.travis.yml +0 -14
  59. data/lib/did_you_mean/experimental/initializer_name_correction.rb +0 -20
  60. data/lib/did_you_mean/experimental/ivar_name_correction.rb +0 -74
  61. data/lib/did_you_mean/verbose_formatter.rb +0 -7
  62. data/test/core_ext/name_error_extension_test.rb +0 -47
  63. data/test/deprecated_formatter_test.rb +0 -9
  64. data/test/experimental/initializer_name_correction_test.rb +0 -15
  65. data/test/experimental/method_name_checker_test.rb +0 -13
  66. data/test/test_helper.rb +0 -13
  67. data/test/verbose_formatter_test.rb +0 -23
@@ -0,0 +1,20 @@
1
+ # HumanTypo API
2
+ ## Description
3
+ Simulate an error prone human typist. Assumes typographical errors are Poisson distributed and
4
+ each error is either a deletion, insertion, substitution, or transposition
5
+ ## Initialization
6
+ ```
7
+ def initialize(input, lambda: 0.05)
8
+ end
9
+ ```
10
+ where
11
+ ### input: A string with the word to be corrupted.
12
+ ## lambda: Error rate of the poisson process
13
+ The default of 0.05 corresponds to one error every 20 characters, and is thought to approximate the average, competent typist
14
+ ## Methods
15
+ ```
16
+ def call
17
+ end
18
+ ```
19
+ Returns a word with typographical errors.
20
+
@@ -0,0 +1,82 @@
1
+ # TreeSpellChecker Algorithm
2
+ ## Overview
3
+ The algorithm is designed to work on a dictionary that has a rooted tree structure.
4
+
5
+ The algorithm treats the problem as a hidden state system, which tries to identify the true state of the input. Due to typographical errors, the state of the input is a hidden version of the true system state. Each word in the dictionary is mapped to a multi-dimensional state, with the first dimension being being the root, the second dimension being the next branch, and so on. Each dimension is discrete with a finite number of elements. The first dimension corresponds to the root, so only has one element.
6
+
7
+ The algorithm assumes the input state has the correct structure, and so generates the state of the input. It starts with the root of the input word and maps it to the root element. It then looks at the value of second dimension of the input word and chooses closest elements of the possible second dimension elements. It then continues to the third and higher dimensions. It terminates when it has worked out possible elements corresponding to the highest dimension of the input word. At this point it has the possible elements at for each dimension of the input word. It then generates all possible legitimate states from these elements. Finally it then compares the possible leaves at the end of these legitimate states with the leaf of the input word. From this process it produces suggested states.
8
+
9
+ ## Accuracy
10
+ The accuracy of the algorithm was tested using the HumanTypo class. It simulates a human typist by assuming that errors are Poisson distributed at a rate of one typo per 20 characters. Typos can be either a deletion, an insertion, substitution or a transposition.
11
+
12
+ I ran 10,000 repititions on both the `test` directory of the ```did_you_mean``` gem and on the ```spec``` directory of the ```rspec-core``` gem.
13
+
14
+ The results were as follows:
15
+ ```
16
+ Minitest Summary
17
+ --------------------------------------------------------------------------------
18
+ Method | First Time (%) Mean Suggestions Failures (%)
19
+ --------------------------------------------------------------------------------
20
+ Tree 98.0 1.1 2.0
21
+ Standard 98.1 2.2 1.6
22
+ Augmented 100.0 1.1 0.0
23
+ ```
24
+ and
25
+ ```
26
+ Rspec Summary
27
+ --------------------------------------------------------------------------------
28
+ Method | First Time (%) Mean Suggestions Failures (%)
29
+ --------------------------------------------------------------------------------
30
+ Tree 94.7 1.0 5.3
31
+ Standard 98.2 4.2 1.1
32
+ Augmented 99.7 1.2 0.2
33
+ ```
34
+ As well, I checked the results on the ```test``` directory with ```HumanTypo``` generating errors at three times the rate:
35
+ ```
36
+ Minitest Summary (lambda = 0.15)
37
+ --------------------------------------------------------------------------------
38
+ Method | First Time (%) Mean Suggestions Failures (%)
39
+ --------------------------------------------------------------------------------
40
+ Tree 88.9 1.0 11.0
41
+ Standard 95.0 1.4 4.3
42
+ Combined 99.0 1.0 0.8
43
+ ```
44
+ In all cases, the tree speller, when augmented by the standard spell checker performed with higher accuracy, and giving far fewer suggestions.
45
+
46
+ ## Execution Speed
47
+
48
+ I tested the execution time on the ```test ``` directory:
49
+ ```
50
+ Testing execution time of Standard
51
+ Average time (ms): 5.2
52
+
53
+ Testing execution time of Tree
54
+ Average time (ms): 1.1
55
+
56
+ Testing execution time of Augmented Tree
57
+ Average time (ms): 1.2
58
+ ```
59
+ and on the ```spec``` directory
60
+ ```
61
+
62
+ Testing execution time of Standard
63
+ Average time (ms): 40.6
64
+
65
+ Testing execution time of Tree
66
+ Average time (ms): 2.7
67
+
68
+ Testing execution time of Augmented Tree
69
+ Average time (ms): 4.5
70
+ ```
71
+
72
+ I was surprised by how much faster the tree checker was compared to the standard checker. I think the reason is that the predominant computational load will scale with O(log n) where n is the total number of words in the dictionary. My reasoning is that the algorithm very quickly prunes out states as it moves through the dimensions.
73
+ ## Augmentation option
74
+ Given the major difference in speed between the standard and tree checker, and the likelihood that the disparity will grow rapidly with the size of the dictionary, then I suspect for some applications, it will not be not practicable to augment the tree checker by using the standard checker when the tree checker fails to find a suggestion. Accordingly, I have added an option, ```:augment?```. The default is nil, but if true, then the standard checker is used if there are no suggestions.
75
+ ## Generation of Performance data
76
+ This is done using ```test/tree_spell/explore_test.rb```. This is not a proper test file in that there are no assertions in it. As well, it takes over ten minutes to run, accordingly, I have disabled it by setting the constant TREE_SPELL_EXPLORE to false at the top of the file. To run the file, set TREE_SPELL_EXPLORE to true. It is also possible to run quick assessments by using a smaller value of n_repeat in the various tests.
77
+ ## Future Work
78
+ I have identified two categories of remaining errors. The first class is when one of the elements is corrupted to being very small. Then the standard checker does not suggest the correct element, e.g. if an element is ```core``` and it is reduced to ```co```, the standard speller will not make a suggestion. The second class of error is when the structure of the word is broken because one of the separators has been removed. I think it might be possible to remove the first type of error and dramatically reduce the second type of error in a future version. This would be done as follows:
79
+ - At each level, choose the dictionary element with the smallest distance.
80
+ - This will work well unless the structure is broken, in which case it could return a wildly wrong suggestion.
81
+ - To guard against this, the suggestion could be checked against the input word using the standard checker. If the standard checker rejects the suggestion, then it is assumed the structure is broken.
82
+ - A large proportion of the time, a broken structure will be just due to one separator missing, and the order of the elements will not be affected. Accordingly, the structure can be fixed by comparing the input elements with a concatenation of two levels of the dictionary elements. It would be possible to use the same idea to fix more than one separator missing, but this could quickly become computationally expensive.
@@ -0,0 +1,24 @@
1
+ # TreeSpellChecker API
2
+ ## Description
3
+
4
+ ## Initialization
5
+ ```
6
+ def initialize(dictionary:, separator: '/', augment: nil)
7
+ end
8
+ ```
9
+ where
10
+ ####dictionary: The dictionary is a list of possible words
11
+ * that are used to correct a misspelling
12
+ * The dictionary must be tree structured with a single character separator
13
+ * e.g 'spec/models/goals_spec_rb'.
14
+ ####separator: A single charactor. Cannot be cannot be alphabetical, '@' or '.'.
15
+ ####augment: When set to true, the checker will used the standard ```SpellChecker``` to find possible suggestions.
16
+ ## Methods
17
+ ```
18
+ def correct(input)
19
+ end
20
+ ```
21
+ where
22
+ ####input: Is the input word to be corrected.
23
+
24
+
@@ -1,69 +1,112 @@
1
- require "did_you_mean/version"
2
- require "did_you_mean/core_ext/name_error"
1
+ require_relative "did_you_mean/version"
2
+ require_relative "did_you_mean/core_ext/name_error"
3
3
 
4
- require "did_you_mean/spell_checker"
5
- require 'did_you_mean/spell_checkers/name_error_checkers'
6
- require 'did_you_mean/spell_checkers/method_name_checker'
7
- require 'did_you_mean/spell_checkers/key_error_checker'
8
- require 'did_you_mean/spell_checkers/null_checker'
9
-
10
- require "did_you_mean/formatters/plain_formatter"
4
+ require_relative "did_you_mean/spell_checker"
5
+ require_relative 'did_you_mean/spell_checkers/name_error_checkers'
6
+ require_relative 'did_you_mean/spell_checkers/method_name_checker'
7
+ require_relative 'did_you_mean/spell_checkers/key_error_checker'
8
+ require_relative 'did_you_mean/spell_checkers/null_checker'
9
+ require_relative 'did_you_mean/spell_checkers/require_path_checker'
10
+ require_relative 'did_you_mean/formatters/plain_formatter'
11
+ require_relative 'did_you_mean/tree_spell_checker'
11
12
 
13
+ # The +DidYouMean+ gem adds functionality to suggest possible method/class
14
+ # names upon errors such as +NameError+ and +NoMethodError+. In Ruby 2.3 or
15
+ # later, it is automatically activated during startup.
16
+ #
17
+ # @example
18
+ #
19
+ # methosd
20
+ # # => NameError: undefined local variable or method `methosd' for main:Object
21
+ # # Did you mean? methods
22
+ # # method
23
+ #
24
+ # OBject
25
+ # # => NameError: uninitialized constant OBject
26
+ # # Did you mean? Object
27
+ #
28
+ # @full_name = "Yuki Nishijima"
29
+ # first_name, last_name = full_name.split(" ")
30
+ # # => NameError: undefined local variable or method `full_name' for main:Object
31
+ # # Did you mean? @full_name
32
+ #
33
+ # @@full_name = "Yuki Nishijima"
34
+ # @@full_anme
35
+ # # => NameError: uninitialized class variable @@full_anme in Object
36
+ # # Did you mean? @@full_name
37
+ #
38
+ # full_name = "Yuki Nishijima"
39
+ # full_name.starts_with?("Y")
40
+ # # => NoMethodError: undefined method `starts_with?' for "Yuki Nishijima":String
41
+ # # Did you mean? start_with?
42
+ #
43
+ # hash = {foo: 1, bar: 2, baz: 3}
44
+ # hash.fetch(:fooo)
45
+ # # => KeyError: key not found: :fooo
46
+ # # Did you mean? :foo
47
+ #
48
+ #
49
+ # == Disabling +did_you_mean+
50
+ #
51
+ # Occasionally, you may want to disable the +did_you_mean+ gem for e.g.
52
+ # debugging issues in the error object itself. You can disable it entirely by
53
+ # specifying +--disable-did_you_mean+ option to the +ruby+ command:
54
+ #
55
+ # $ ruby --disable-did_you_mean -e "1.zeor?"
56
+ # -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
57
+ #
58
+ # When you do not have direct access to the +ruby+ command (e.g.
59
+ # +rails console+, +irb+), you could applyoptions using the +RUBYOPT+
60
+ # environment variable:
61
+ #
62
+ # $ RUBYOPT='--disable-did_you_mean' irb
63
+ # irb:0> 1.zeor?
64
+ # # => NoMethodError (undefined method `zeor?' for 1:Integer)
65
+ #
66
+ #
67
+ # == Getting the original error message
68
+ #
69
+ # Sometimes, you do not want to disable the gem entirely, but need to get the
70
+ # original error message without suggestions (e.g. testing). In this case, you
71
+ # could use the +#original_message+ method on the error object:
72
+ #
73
+ # no_method_error = begin
74
+ # 1.zeor?
75
+ # rescue NoMethodError => error
76
+ # error
77
+ # end
78
+ #
79
+ # no_method_error.message
80
+ # # => NoMethodError (undefined method `zeor?' for 1:Integer)
81
+ # # Did you mean? zero?
82
+ #
83
+ # no_method_error.original_message
84
+ # # => NoMethodError (undefined method `zeor?' for 1:Integer)
85
+ #
12
86
  module DidYouMean
13
- class DeprecatedIgnoredCallers < Array
14
- %i(
15
- +
16
- <<
17
- []=
18
- insert
19
- unshift
20
- push
21
- ).each do |method_name|
22
- eval <<-RUBY, nil, __FILE__, __LINE__ + 1
23
- def #{method_name}(*)
24
- warn "IGNORED_CALLERS has been deprecated and has no effect."
87
+ # Map of error types and spell checker objects.
88
+ SPELL_CHECKERS = Hash.new(NullChecker)
25
89
 
26
- super
27
- end
28
- RUBY
29
- end
90
+ # Adds +DidYouMean+ functionality to an error using a given spell checker
91
+ def self.correct_error(error_class, spell_checker)
92
+ SPELL_CHECKERS[error_class.name] = spell_checker
93
+ error_class.prepend(Correctable) unless error_class < Correctable
30
94
  end
31
95
 
32
- IGNORED_CALLERS = DeprecatedIgnoredCallers.new
33
-
34
- SPELL_CHECKERS = Hash.new(NullChecker)
35
- SPELL_CHECKERS.merge!({
36
- "NameError" => NameErrorCheckers,
37
- "NoMethodError" => MethodNameChecker,
38
- "KeyError" => KeyErrorChecker
39
- })
40
-
41
- NameError.prepend DidYouMean::Correctable
42
- KeyError.prepend DidYouMean::Correctable
96
+ correct_error NameError, NameErrorCheckers
97
+ correct_error KeyError, KeyErrorChecker
98
+ correct_error NoMethodError, MethodNameChecker
99
+ correct_error LoadError, RequirePathChecker if RUBY_VERSION >= '2.8.0'
43
100
 
101
+ # Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+.
44
102
  def self.formatter
45
103
  @@formatter
46
104
  end
47
105
 
106
+ # Updates the primary formatter used to format the suggestions.
48
107
  def self.formatter=(formatter)
49
108
  @@formatter = formatter
50
109
  end
51
110
 
52
111
  self.formatter = PlainFormatter.new
53
-
54
- # Deprecated formatter
55
- class Formatter #:nodoc:
56
- def initialize(corrections = [])
57
- @corrections = corrections
58
- end
59
-
60
- def to_s
61
- return "" if @corrections.empty?
62
-
63
- output = "\nDid you mean? ".dup
64
- output << @corrections.join("\n ")
65
- end
66
- end
67
-
68
- deprecate_constant :Formatter
69
112
  end
@@ -6,22 +6,20 @@ module DidYouMean
6
6
 
7
7
  def to_s
8
8
  msg = super.dup
9
+ suggestion = DidYouMean.formatter.message_for(corrections)
9
10
 
10
- if !cause.respond_to?(:corrections) || cause.corrections.empty?
11
- msg << DidYouMean.formatter.message_for(corrections)
12
- end
13
-
11
+ msg << suggestion if !msg.end_with?(suggestion)
14
12
  msg
15
13
  rescue
16
14
  super
17
15
  end
18
16
 
19
17
  def corrections
20
- spell_checker.corrections
18
+ @corrections ||= spell_checker.corrections
21
19
  end
22
20
 
23
21
  def spell_checker
24
- @spell_checker ||= SPELL_CHECKERS[self.class.to_s].new(self)
22
+ SPELL_CHECKERS[self.class.to_s].new(self)
25
23
  end
26
24
  end
27
25
  end
@@ -1,2 +1,2 @@
1
- require 'did_you_mean/experimental/initializer_name_correction'
2
- require 'did_you_mean/experimental/ivar_name_correction'
1
+ warn "Experimental features in the did_you_mean gem has been removed " \
2
+ "and `require \"did_you_mean/experimental\"' has no effect."
@@ -1,7 +1,31 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module DidYouMean
4
+ # The +DidYouMean::PlainFormatter+ is the basic, default formatter for the
5
+ # gem. The formatter responds to the +message_for+ method and it returns a
6
+ # human readable string.
4
7
  class PlainFormatter
8
+
9
+ # Returns a human readable string that contains +corrections+. This
10
+ # formatter is designed to be less verbose to not take too much screen
11
+ # space while being helpful enough to the user.
12
+ #
13
+ # @example
14
+ #
15
+ # formatter = DidYouMean::PlainFormatter.new
16
+ #
17
+ # # displays suggestions in two lines with the leading empty line
18
+ # puts formatter.message_for(["methods", "method"])
19
+ #
20
+ # Did you mean? methods
21
+ # method
22
+ # # => nil
23
+ #
24
+ # # displays an empty line
25
+ # puts formatter.message_for([])
26
+ #
27
+ # # => nil
28
+ #
5
29
  def message_for(corrections)
6
30
  corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}"
7
31
  end
@@ -1,7 +1,43 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module DidYouMean
4
+ # The +DidYouMean::VerboseFormatter+ uses extra empty lines to make the
5
+ # suggestion stand out more in the error message.
6
+ #
7
+ # In order to activate the verbose formatter,
8
+ #
9
+ # @example
10
+ #
11
+ # OBject
12
+ # # => NameError: uninitialized constant OBject
13
+ # # Did you mean? Object
14
+ #
15
+ # require 'did_you_mean/verbose'
16
+ #
17
+ # OBject
18
+ # # => NameError: uninitialized constant OBject
19
+ # #
20
+ # # Did you mean? Object
21
+ # #
22
+ #
4
23
  class VerboseFormatter
24
+
25
+ # Returns a human readable string that contains +corrections+. This
26
+ # formatter is designed to be less verbose to not take too much screen
27
+ # space while being helpful enough to the user.
28
+ #
29
+ # @example
30
+ #
31
+ # formatter = DidYouMean::PlainFormatter.new
32
+ #
33
+ # puts formatter.message_for(["methods", "method"])
34
+ #
35
+ #
36
+ # Did you mean? methods
37
+ # method
38
+ #
39
+ # # => nil
40
+ #
5
41
  def message_for(corrections)
6
42
  return "" if corrections.empty?
7
43
 
@@ -41,7 +41,7 @@ module DidYouMean
41
41
 
42
42
  # detects the minimum value out of three arguments. This method is
43
43
  # faster than `[a, b, c].min` and puts less GC pressure.
44
- # See https://github.com/yuki24/did_you_mean/pull/1 for a performance
44
+ # See https://github.com/ruby/did_you_mean/pull/1 for a performance
45
45
  # benchmark.
46
46
  def min3(a, b, c)
47
47
  if a < b && a < c
@@ -1,11 +1,11 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require "did_you_mean/levenshtein"
4
- require "did_you_mean/jaro_winkler"
3
+ require_relative "levenshtein"
4
+ require_relative "jaro_winkler"
5
5
 
6
6
  module DidYouMean
7
7
  class SpellChecker
8
- def initialize(dictionary: )
8
+ def initialize(dictionary:)
9
9
  @dictionary = dictionary
10
10
  end
11
11
 
@@ -13,14 +13,14 @@ module DidYouMean
13
13
  input = normalize(input)
14
14
  threshold = input.length > 3 ? 0.834 : 0.77
15
15
 
16
- words = @dictionary.select {|word| JaroWinkler.distance(normalize(word), input) >= threshold }
17
- words.reject! {|word| input == word.to_s }
18
- words.sort_by! {|word| JaroWinkler.distance(word.to_s, input) }
16
+ words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), input) >= threshold }
17
+ words.reject! { |word| input == word.to_s }
18
+ words.sort_by! { |word| JaroWinkler.distance(word.to_s, input) }
19
19
  words.reverse!
20
20
 
21
21
  # Correct mistypes
22
22
  threshold = (input.length * 0.25).ceil
23
- corrections = words.select {|c| Levenshtein.distance(normalize(c), input) <= threshold }
23
+ corrections = words.select { |c| Levenshtein.distance(normalize(c), input) <= threshold }
24
24
 
25
25
  # Correct misspells
26
26
  if corrections.empty?
@@ -38,13 +38,7 @@ module DidYouMean
38
38
  private
39
39
 
40
40
  def normalize(str_or_symbol) #:nodoc:
41
- str = if str_or_symbol.is_a?(String)
42
- str_or_symbol.dup
43
- else
44
- str_or_symbol.to_s
45
- end
46
-
47
- str.downcase!
41
+ str = str_or_symbol.to_s.downcase
48
42
  str.tr!("@", "")
49
43
  str
50
44
  end