Trying to Find Vowels of a String Using Ruby While Loops

Trying to find vowels of a string using Ruby while loops

The problem is that you never reset j to zero.

The first time your outer while loop runs, which is to compare the first character of string to each vowel, j is incremented from 0 (for "a") to 4 (for "u"). The second time the outer loop runs, however, j is already 4, which means it then gets incremented to 5, 6, 7 and on and on. vowels[5], vowels[6], etc. all evaluate to nil, so characters after the first are never counted as vowels.

If you move the j = 0 line inside the outer while loop, your method works correctly.


Your second question, about .each, shows that you're already thinking along the right lines. while is rarely seen in Ruby and .each would definitely be an improvement. As it turns out, you can't call .each on a String (because the String class doesn't include Enumerable), so you have to turn it into an Array of characters first with the String#chars method. With that, your code would look like this:

def count_vowels(string)
chars = string.chars
vowels = ["a", "e", "i", "o", "u"]
count = 0

chars.each do |char|
vowels.each do |vowel|
if char == vowel
count += 1
break
end
end
end

puts count
end

In Ruby, though, we have much better ways to do this sort of thing. One that fits particularly well here is Array#count. It takes a block and evaluates it for each item in the array, then returns the number of items for which the block returned true. Using it we could write a method like this:

def count_vowels(string)
chars = string.chars
vowels = ["a", "e", "i", "o", "u"]

count = chars.count do |char|
is_vowel = false
vowels.each do |vowel|
if char == vowel
is_vowel = true
break
end
end

is_vowel
end

puts count
end

That's not much shorter, though. Another great method we can use is Enumerable#any?. It evaluates the given block for each item in the array and returns true upon finding any item for which the block returns true. Using it makes our code super short, but still readable:

def count_vowels(string)
chars = string.chars
vowels = %w[ a e i o u ]

count = chars.count do |char|
vowels.any? {|vowel| char == vowel }
end

puts count
end

(Here you'll see I threw in another common Ruby idiom, the "percent literal" notation for creating an array: %w[ a e i o u ]. It's a common way to create an array of strings without all of those quotation marks and commas. You can read more about it here.)

Another way to do the same thing would be to use Enumerable#include?, which returns true if the array contains the given item:

def count_vowels(string)
vowels = %w[ a e i o u ]
puts string.chars.count {|char| vowels.include?(char) }
end

...but as it turns out, String has an include? method, too, so we can do this instead:

def count_vowels(string)
puts string.chars.count {|char| "aeiou".include?(char) }
end

Not bad! But I've saved the best for last. Ruby has a great method called String#count:

def count_vowels(string)
puts string.count("aeiou")
end

How to make a hash of all vowels in a string?

I'm just going to include an O(n) solution since the prior are all O(n^2).

str.gsub(/[^aeiou]/, "").each_char.each_with_object(Hash.new(0)) { |vowel, hash| hash[vowel] += 1 }
=> {"a"=>5, "e"=>3, "o"=>2, "i"=>1}

Determining the Big O notation of this isn't perfect, so bear with me.

Step 1 - O(n)

str.gsub(/[^aeiou]/, "")
=> "aeaoeoeaaai"

This step loops through the string characters and removes consonants. I did my best to try and find the actual runtime of gsub but without doing my own benchmarks I can't really be sure. I found this when originally writing my answer but it is also not ironclad. gsub should find all indexes that match the expression and return those values as a new string.

Step 2 - O(n)

each_char

Simply takes the string returned and returns an Enumerator. Depending on the language, turning a string into an array of strings/chars has at worst a runtime of the length of the string (hence O(n)). With ruby, returning an Enumerator is actually lazily evaluated so you could argue here we're in O(1).

Step 3 - O(n)

each_with_object(Hash.new(0)) { |vowel, hash| hash[vowel] += 1 }

There are multiple sub-steps here:

Step 3a - Instantiate a new Hash object O(1) who's default value is 0.

Step 3b - Assign/find the key (vowel) in the hash - average O(1), worst case O(n)

Step 3c - Increment that value by 1 - O(1)

Step 3d - Loop through each letter - O(n)

So that means from our 3 steps we have O(n) + O(n) + O(n) which is really O(3n) but Big O states we can drop constants like 3 in this instance, so it just becomes O(n).

I myself am also still learning Big O so this explanation could likely use some community input.

Function to count vowels in a string

First of all, you have a end missing for your if. I think you wanted to do this (which will not give you the correct answer though):

def count_vowels(string)
result = 0
i = 0
while i < string.length
if string[i] == "a" ||"e" || "i" || "o" || "u"
result = result + 1
i += 1
end
end

return result
end

Try doing this using a regular expression like this:

def count_vowels(string)
string.scan(/[aeouiAEIOU]/).count
end

/[aeouiAEIOU]/ is a regular expression that basically means any of these characters: a, e, o, u, i, A, E, I, O, U.

The String#scan method returns all matches of that regular expression in the string which means you get the number of vowels!

Taking a string and returning it with vowels removed

Finding the bug

Let's see what's wrong with your original code by executing your method's code in IRB:

$ irb
irb(main):001:0> str = "apple"
#=> "apple"
irb(main):002:0> new = str.split(" ")
#=> ["apple"]

Bingo! ["apple"] is not the expected result. What does the documentation for String#split say?

split(pattern=$;, [limit]) → anArray


Divides str into substrings based on a delimiter, returning an array of these substrings.

If pattern is a String, then its contents are used as the delimiter when splitting str. If pattern is a single space, str is split on whitespace, with leading whitespace and runs of contiguous whitespace characters ignored.

Our pattern is a single space, so split returns an array of words. This is definitely not what we want. To get the desired result, i.e. an array of characters, we could pass an empty string as the pattern:

irb(main):003:0> new = str.split("")
#=> ["a", "p", "p", "l", "e"]

"split on empty string" feels a bit hacky and indeed there's another method that does exactly what we want: String#chars

chars → an_array


Returns an array of characters in str. This is a shorthand for str.each_char.to_a.

Let's give it a try:

irb(main):004:0> new = str.chars
#=> ["a", "p", "p", "l", "e"]

Perfect, just as advertised.

Another bug

With the new method in place, your code still doesn't return the expected result (I'm going to omit the IRB prompt from now on):

vowel("apple") #=> "elpp"

This is because

result = new[i] + result

prepends the character to the result string. To append it, we have to write

result = result + new[i]

Or even better, use the append method String#<<:

result << new[i]

Let's try it:

def vowel(str)
result = ""
new = str.chars
i = 0
while i < new.length
if new[i] != "a"
result << new[i]
end
i = i + 1
end
return result
end

vowel("apple") #=> "pple"

That looks good, "a" has been removed ("e" is still there, because you only check for "a").


Now for some refactoring.

Removing the explicit loop counter

Instead of a while loop with an explicit loop counter, it's more idiomatic to use something like Integer#times:

new.length.times do |i|
# ...
end

or Range#each:

(0...new.length).each do |i|
# ...
end

or Array#each_index:

new.each_index do |i|
# ...
end

Let's apply the latter:

def vowel(str)
result = ""
new = str.chars
new.each_index do |i|
if new[i] != "a"
result << new[i]
end
end
return result
end

Much better. We don't have to worry about initializing the loop counter (i = 0) or incrementing it (i = i + 1) any more.

Avoiding character indices

Instead of iterating over the character indices via each_index:

new.each_index do |i|
if new[i] != "a"
result << new[i]
end
end

we can iterate over the characters themselves using Array#each:

new.each do |char|
if char != "a"
result << char
end
end

Removing the character array

We don't even have to create the new character array. Remember the documentation for chars?

This is a shorthand for str.each_char.to_a.

String#each_char passes each character to the given block:

def vowel(str)
result = ""
str.each_char do |char|
if char != "a"
result << char
end
end
return result
end

The return keyword is optional. We could just write result instead of return result, because a method's return value is the last expression that was evaluated.

Removing the explicit string

Ruby even allows you to pass an object into the loop using Enumerator#with_object, thus eliminating the explicit result string:

def vowel(str)
str.each_char.with_object("") do |char, result|
if char != "a"
result << char
end
end
end

with_object passes "" into the block as result and returns it (after the characters have been appended within the block). It is also the last expression in the method, i.e. its return value.

You could also use if as a modifier, i.e.:

result << char if char != "a"

Alternatives

There are many different ways to remove characters from a string.

Another approach is to filter out the vowel characters using Enumerable#reject (it returns a new array containing the remaining characters) and then join the characters (see Nathan's answer for a version to remove all vowels):

def vowel(str)
str.each_char.reject { |char| char == "a" }.join
end

For basic operations like string manipulation however, Ruby usually already provides a method. Check out the other answers for built-in alternatives:

  • str.delete('aeiouAEIOU') as shown in Gagan Gami's answer
  • str.tr('aeiouAEIOU', '') as shown in Cary Swoveland's answer
  • str.gsub(/[aeiou]/i, '') as shown in Avinash Raj's answer

Naming things

Cary Swoveland pointed out that vowel is not the best name for your method. Choose the names for your methods, variables and classes carefully. It's desirable to have a short and succinct method name, but it should also communicate its intent.

vowel(str) obviously has something to do with vowels, but it's not clear what it is. Does it return a vowel or all vowels from str? Does it check whether str is a vowel or contains a vowel?

remove_vowels or delete_vowels would probably be a better choice.

Same for variables: new is an array of characters. Why not call it characters (or chars if space is an issue)?

Bottom line: read the fine manual and get to know your tools. Most of the time, an IRB session is all you need to debug your code.

Advancing Vowel to the Next Vowel in Ruby

Here's your code with the fewest corrections necessary to make it work:

def vowel_adv(str)
vowels = ["a", "e", "i", "o", "u"]
str = str.split('')
str_new = str.map do |char|
if vowels.include?(char)
vowels.rotate(1)[vowels.index(char)]
else
char
end
end
str_new.join
end
vowel_adv "aeiou"
=> "eioua"

Things that I changed include

  • addition of a block variable to the map block
  • returning the thing you're mapping to from the map block
  • include? is called on the Array, not on the possible element
  • finding the next vowel by looking in the array of vowels, not by incrementing the character, which is what I think you were trying to do.

Here's an improved version:

VOWELS = %w(a e i o u)
ROTATED_VOWELS = VOWELS.rotate 1

def vowel_adv(str)
str.
chars.
map do |char|
index = VOWELS.index char
if index
ROTATED_VOWELS[index]
else
char
end
end.
join
end
  • static Arrays in constants
  • nicer array-of-string syntax
  • String#chars instead of split
  • use the index to test for inclusion instead of include?
  • no assignment to parameters, which is a little confusing
  • no temporary variables, which some people like and some people don't but I've done here to show that it's possible

And, just because Ruby is fun, here's a different version which copies the string and modifies the copy:

VOWELS = %w(a e i o u)
ROTATED_VOWELS = VOWELS.rotate 1

def vowel_adv(str)
new_str = str.dup
new_str.each_char.with_index do |char, i|
index = VOWELS.index char
if index
new_str[i] = ROTATED_VOWELS[index]
end
end
new_str
end


Related Topics



Leave a reply



Submit