0

I'm trying to create a method that takes a string as input, and if the string contains one, two, three, four, five, six, seven, eight, or nine, counts them as integers and adds them, for example:

secret_code("one on one") #=> 2
secret_code("five monkeys in three farms") #=> 8
secret_code("") #=> 0
secret_code("fivethreeone") #=> 0

I wanted to split the given string by words and put them into an array so that I could iterate it and check if the element at position i equals to a number, and add its corresponding value to a variable. This is my code so far:

def secret_code(a)
  arr = a.split(/\W+/)
  i = 0
  l = arr.size
  num = 0
  puts arr
  if !arr.any?
    0
  end
  while i < l do 
    case arr
      when arr[i] == "one"
        num += 1
        exit
      when arr[i] == "two"
        num += 2
        exit
      when arr[i] == "three"
        num += 3
        exit
      when arr[i] == "four"
        num += 4
        exit
      when arr[i] == "five"
        num += 5
        exit
      when arr[i] == "six"
        num += 6
        exit
      when arr[i] == "seven"
        num += 7
        exit
      when arr[i] == "eight"
        num += 8
        exit
      when arr[i] == "nine"
        num += 9
        exit
    end
    i+=1
  end
  puts num
end

secret_code("one on one") #=> 2
secret_code("five monkeys in three farms") #=> 8
secret_code("") #=> 0
secret_code("fivethreeone") #=> 0

I'm not quite sure how to iterate over the array. Right now it prints 0 for everything.

sawa
  • 165,429
  • 45
  • 277
  • 381
Sebastian Delgado
  • 706
  • 3
  • 7
  • 26
  • 1
    Your `case` statement seems to have some flaws - have a look at this - [How to write case statement in Ruby](http://stackoverflow.com/questions/948135/how-can-i-write-a-switch-statement-in-ruby) - and don't use `exit` - it will exit from program prematurely. If you fix your `case` and get rid of `exit`, you will correct output – Wand Maker Dec 11 '15 at 18:42
  • 1
    Why the rush to select an answer? You may discourage other interesting answers by doing so, and imo it is inconsiderate to readers still working on their answers when you made the selection. Many SO members wait at least a couple of hours--sometimes much longer--before making a selection. – Cary Swoveland Dec 11 '15 at 20:17
  • Many wait a couple days. Good answers take a while to create, plus people who answer are spread across the globe, or only get on Stack Overflow every few days. – the Tin Man Dec 11 '15 at 20:53
  • The basic problem is answered in the duplicated question's selected answer. The rest of this problem is how to match words to decide what is a "number word" and what isn't, and that's easily done with a hash or array of acceptable values. – the Tin Man Dec 11 '15 at 21:04

5 Answers5

2

The problem is here:

case arr
  when arr[i] == "one"
    num += 1
  # ...
end

In a case statement, the value you give after each when will be compared to the value you give after case. In other words, your case statement will first do (arr[i] == "one") === arr, then (arr[i] == "two") === arr, and so on. Since arr[i] == "one" returns true or false, and true === arr and false === arr will never be true, none of the conditions in your case statement are ever met.

The simplest fix is this:

case arr[i]
  when "one"
    num += 1
  when "two"
    num += 2
  when "three"
    num += 3
  # ...
end

What this code does is compare the value of arr[i] to "one", then "two", and so on, which yields the intended behavior.

Note also that I removed exit. In Ruby, exit exits the program. I'm guessing you were trying to use it like you'd use break in other programming languages. You don't need that here, because Ruby case statements don't have "fall-through" like switch does in some languages. Only one when will match in a case statement.

More improvements

I have a few more notes on your code:

  1. This code doesn't do anything:

    if !arr.any?
      0
    end
    

    You don't do anything with that 0, so it just disappears into the void. You probably meant to do return 0 to return from the method early. A better idiom would have been this:

    return 0 if arr.empty?
    
  2. while is unnecessary here In Ruby it's much more common to use Enumerable#each to iterate over elements of an array:

    arr.each do |word|
      case word
        when "one"
          # ...
        # ...
      end
    end
    
  3. You can make your case statement much more concise by treating it as an expression that returns a value:

    num += case arr[i]
      when "one"   then 1
      when "two"   then 2
      when "three" then 3
      # ...
      else 0
    end
    
  4. You can make this more concise still by using a Hash instead of a case statement:

    word_values = {
      "one"   => 1, "two"   => 2, "three" => 3,
      "four"  => 4, "five"  => 5, "six"   => 6,
      "seven" => 7, "eight" => 8, "nine"  => 9
    }
    word_values.default = 0
    
    arr.each do |word|
      num += word_values[word]
    end
    

    This iterates over the array of words and looks up each one in the word_values hash. If the word isn't in the hash it will return the default value, 0.

  5. With the word_values hash you could also do the following:

    values = arr.map {|word| word_values[word] }
    puts values.reduce(:+)
    

    This uses map to transform the array of words into an array of values, for example "five monkeys in three farms" becomes [5, 0, 0, 3, 0]. It then uses reduce (also called inject) to calculate the sum of the values.

All together now

Put all of those improvements together and we have this:

WORD_VALUES = {
  "one"   => 1, "two"   => 2, "three" => 3,
  "four"  => 4, "five"  => 5, "six"   => 6,
  "seven" => 7, "eight" => 8, "nine"  => 9
}
WORD_VALUES.default = 0

def secret_code(str)
  str.split(/\W+/)
    .map {|word| WORD_VALUES[word] }
    .reduce(:+) || 0
end

puts secret_code("one on one") # => 2
puts secret_code("five monkeys in three farms") # => 8
puts secret_code("") # => 0
puts secret_code("fivethreeone") # => 0

(In Ruby 2.3 you can actually do .map(&WORD_VALUES), which is pretty neat.)

And here's another approach, which is slightly more efficient:

def secret_code(str)
  str.split(/\W+/).reduce(0) {|sum, word| sum + WORD_VALUES[word] }
end

It uses reduce in its block form to look up each value in the hash and add it to a running sum.

An alternative

This is slightly more "advanced," but shows off some other Ruby features:

WORD_VALUES = {
  "one"   => 1, "two"   => 2, "three" => 3,
  "four"  => 4, "five"  => 5, "six"   => 6,
  "seven" => 7, "eight" => 8, "nine"  => 9
}

WORD_EXPR = /\b#{Regexp.union(WORD_VALUES.keys)}\b/

def secret_code(str)
  str.scan(WORD_EXPR).reduce(0) {|sum, word| sum + WORD_VALUES[word] }
end

This uses the keys of the WORD_VALUES array to build a regular expression that looks (more or less) like this:

WORD_EXPR = /\b(one|two|three|four|five|six|seven|eight|nine)\b/

Then it uses String#scan, which returns all of the substrings that match the regular expression, so e.g. "five monkeys in three farms".scan(WORD_EXPR) returns ["five", "three"]. Then it uses reduce as above to look up their values and sum them.

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
2

I would do something like this:

NUMBERS = {
  'one'   => 1, 'two'   => 2, 'three' => 3,
  'four'  => 4, 'five'  => 5, 'six'   => 6,
  'seven' => 7, 'eight' => 8, 'nine'  => 9
}

def secret_code(string)
  words = string.split(/\W+/)

  words.inject(0) do |sum, word|
    sum + NUMBERS.fetch(word, 0)
  end
end

puts secret_code('one on one')
#=> 2
puts secret_code('five monkeys in three farms')
#=> 8
puts secret_code('')
#=> 0
puts secret_code('fivethreeone')
#=> 0

NUMBERS.fetch(word, 0) tries to get a value from the NUMBERS hash, it returns 0 if the key word does not exist. It basically does the same than:

if NUMBERS.key?(word)
  0
else
  NUMBERS[word]
end

inject(0) do |sum, word| is initialized with 0. It iterates over each element in an array and passes the current value as sum to the block. The result of the block (sum + the value of the word) is used as new value of sum in the next iteration.

spickermann
  • 100,941
  • 9
  • 101
  • 131
2

Your case statement is wrong as it contains exit that in Ruby causes the immediate termination of the running script.

You should check the proper syntax of the case statement.

Moreover, you can use scan to quickly scan your string for matches.

"five monkeys in three farms".scan(/five|three|four/)
 => ["five", "three"]

And then you can convert the matches into the corresponding values (for example with a map stored in a Hash) and compute the result.

numbers = {
    "three" => 3,
    "four" => 4,
    "five" => 5,
}

results = "five monkeys in three farms".scan(/#{numbers.keys.join("|")}/)
 => ["five", "three"]

results.inject(0) { |total, result| total += numbers[result] }
# => 8

There is one caveat with this solution. It will match partial words, therefore you will need to adapt it to consider full words using the appropriate boundaries if it applies (hence if someone can send strings like the last one you provided in the example).

Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
2

The problem with your code has been answered by others. Here's an alternative.

str = "five monkeys in three farms"

h = { "one=>1", "two"=>2, "three"=>3, "four"=>4, "five"=>5,
      "six"=>6, "seven"=>7, "eight"=>8, "nine"=>9 }

str.gsub(/\w+/,h).split.map(&:to_i).reduce(:+)
  #=> 8

This uses the form of Hash.gsub that employs a hash. The steps:

s = str.gsub(/\w+/,h)
  #=> "5   3 "

Matches that are not keys of h are converted to empty strings. Spaces do not match \w+/ so they are unaffected.

t = s.split
  #=> ["5", "3"] 
u = t.map(&:to_i)
  #=> [5, 3] 
u.reduce(:+)
  #=> 8 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • 1
    That's not the easiest thing to puzzle out for a beginner. Could you add some explanation? – Schwern Dec 11 '15 at 19:01
  • 1
    I agree it may be hard to read for a beginner and it would be helpful to elaborate it, but I'm voting +1 because it's a pretty clever and nice implementation. It's important to note that, however, it doesn't really answer why the user is experiencing the problem with his code. ;) – Simone Carletti Dec 11 '15 at 19:06
  • I always try to provide explanations, but sometimes, as here, I present the code alone and then edit to provide the explanation. – Cary Swoveland Dec 11 '15 at 19:20
1

There are many functions to search strings in Ruby. You don't have to do it manually and its rare you'd want to. For this you'd use String#scan to find words.

str.scan(/(\w+)/) { |word|
    ... do something with word[0] ...
}

Then you need to turn those words into numbers. There's several ways to do it provided in "Convert string numbers( in word format) to integer ruby". For production code you should use a gem, but since this is an exercise in scanning strings the simplest is to use a little table.

str = "one two five and zero"

# Map words to their numbers.
word2num = {
    "one" => 1, "two" => 2, "three" => 3, "four" => 4, "five" => 5,
    "six" => 6, "seven" => 7, "eight" => 8, "nine" => 9, "ten" => 10,
    "eleven" => 11, "twelve" => 12
};
# Because there are words which are not numbers, set the default
# for the hash to 0.  If you try to put in a word that's not a
# number you'll get 0 instead of nil (which would cause an error).
word2num.default = 0

number = 0;
str.scan(/(\w+)/) { |word|
    number += word2num[word[0]]
}
puts number
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • 1
    What if the string were `"eighteen monkeys"`, which should return zero? – Cary Swoveland Dec 11 '15 at 19:03
  • @CarySwoveland Like I said, in real code you'd use a gem. This isn't about adding numbers, there's already a whole other question for that which I linked to, it's about teaching how to scan strings in Ruby. – Schwern Dec 11 '15 at 19:10